= { // TS 语法
+ // const editorConfig = { // JS 语法
+ placeholder: '请输入内容...',
+ }
+
+ // 及时销毁 editor ,重要!
+ useEffect(() => {
+ return () => {
+ if (editor == null) return
+ editor.destroy()
+ setEditor(null)
+ }
+ }, [editor])
+
+ return (
+ <>
+
+
+ setHtmlFn(editor.getHtml())}
+ mode="default"
+ style={{ height: '500px', overflowY: 'hidden' }}
+ />
+
+ >
+ )
+}
+export default WangEditor
\ No newline at end of file
diff --git a/webook-fe/src/pages/articles/edit.tsx b/webook-fe/src/pages/articles/edit.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e951a04b43b007b46ba2ca7c324835be1fd5dbea
--- /dev/null
+++ b/webook-fe/src/pages/articles/edit.tsx
@@ -0,0 +1,92 @@
+import dynamic from 'next/dynamic'
+import {Button, Form, Input} from "antd";
+import {useEffect, useState} from "react";
+import axios from "@/axios/axios";
+import router from "next/router";
+import {ProLayout} from "@ant-design/pro-components";
+import {useSearchParams} from "next/navigation";
+const WangEditor = dynamic(
+ // 引入对应的组件 设置的组件参考上面的wangEditor react使用文档
+ () => import('../../components/editor'),
+ {ssr: false},
+)
+
+function Page() {
+ const [form] = Form.useForm()
+ const [html, setHtml] = useState()
+ const params = useSearchParams()
+ const artID = params?.get("id")
+ const onFinish = (values: any) => {
+ if(artID) {
+ values.id = parseInt(artID)
+ }
+ values.content = html
+ axios.post("/articles/edit", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ if (res.data?.code == 0) {
+ router.push('/articles/list')
+ return
+ }
+ alert(res.data?.msg || "系统错误");
+ }).catch((err) => {
+ alert(err);
+ })
+ };
+ const publish = () => {
+ const values = form.getFieldsValue()
+ if (artID) {
+ values.id = parseInt(artID)
+ }
+ values.content = html
+ axios.post("/articles/publish", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ if (res.data?.code == 0) {
+ router.push('/articles/view?id='+res.data.data)
+ return
+ }
+ alert(res.data?.msg || "系统错误");
+ }).catch((err) => {
+ alert(err);
+ })
+ }
+
+ const [data, setData] = useState( )
+ useEffect(() => {
+ if (!artID) {
+ return
+ }
+ axios.get('/articles/detail/'+artID)
+ .then((res) => res.data)
+ .then((data) => {
+ form.setFieldsValue(data.data)
+ setHtml(data.data.content)
+ })
+ }, [form, artID])
+
+ return
+
+
+
+
+
+
+
+
+
+
+
+}
+export default Page
\ No newline at end of file
diff --git a/webook-fe/src/pages/articles/list.tsx b/webook-fe/src/pages/articles/list.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4a8d0e1303a38f2a822c8aa5dac51318f3e985df
--- /dev/null
+++ b/webook-fe/src/pages/articles/list.tsx
@@ -0,0 +1,137 @@
+import {EditOutlined} from '@ant-design/icons';
+import {ProLayout, ProList} from '@ant-design/pro-components';
+import {Button, Tag} from 'antd';
+import React, {useEffect, useState} from 'react';
+import axios from "@/axios/axios";
+import router from "next/router";
+
+const IconText = ({ icon, text, onClick }: { icon: any; text: string, onClick: any}) => (
+
+);
+
+interface ArticleItem {
+ id: bigint
+ title: string
+ status: number
+ abstract: string
+}
+
+const dataSource = [
+ {
+ title: '语雀的天空',
+ status: 1,
+ content: "hello, world"
+ },
+ {
+ title: 'Ant Design',
+ },
+ {
+ title: '蚂蚁金服体验科技',
+ },
+ {
+ title: 'TechUI',
+ },
+];
+
+const ArticleList = () => {
+ const [data, setData] = useState>([])
+ const [loading, setLoading] = useState()
+ useEffect(() => {
+ setLoading(true)
+ axios.post('/articles/list', {
+ "offset": 0,
+ "limit": 100,
+ }).then((res) => res.data)
+ .then((data) => {
+ setData(data.data)
+ setLoading(false)
+ })
+ }, [])
+
+ return (
+
+
+ toolBarRender={() => {
+ return [
+ ,
+ ];
+ }}
+ itemLayout="vertical"
+ rowKey="id"
+ headerTitle="文章列表"
+ loading={loading}
+ dataSource={data}
+ // ts:ignore
+ metas={{
+ title: {
+ dataIndex: "title"
+ },
+ description: {
+ render: (data, record, idx) => {
+ switch (record.status) {
+ case 1:
+ return (
+ <>
+ 未发表
+ >
+ )
+ case 2:
+ return (
+ <>
+ 已发表
+ >
+ )
+ case 3:
+ return (
+ <>
+ 尽自己可见
+ >
+ )
+ default:
+ return (<>>)
+ }
+
+ },
+ },
+ actions: {
+ render: (text, row) => [
+ {
+ router.push("/articles/edit?id=" + row.id.toString())
+ }}
+ key="list-vertical-edit-o"
+ />,
+ ],
+ },
+ extra: {
+ render: () => (
+
+ ),
+ },
+ content: {
+ render: (node, record) => {
+ return (
+
+
+
+ )
+ }
+ },
+ }}
+ />
+
+ );
+};
+
+export default ArticleList;
diff --git a/webook-fe/src/pages/articles/model.tsx b/webook-fe/src/pages/articles/model.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..722bd753bf8845db08844c0f27e5efdb57092884
--- /dev/null
+++ b/webook-fe/src/pages/articles/model.tsx
@@ -0,0 +1,10 @@
+type Article = {
+ id: number
+ title: string
+ content: string
+ likeCnt: number
+ liked: boolean
+ collectCnt: number
+ collected: boolean
+ readCnt: number
+}
\ No newline at end of file
diff --git a/webook-fe/src/pages/articles/view.tsx b/webook-fe/src/pages/articles/view.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..df28bd2f8c81acfa5cad8fd05bfdecb829b1094a
--- /dev/null
+++ b/webook-fe/src/pages/articles/view.tsx
@@ -0,0 +1,83 @@
+import React, {useState, useEffect, CSSProperties} from 'react';
+import axios from "@/axios/axios";
+import {useSearchParams} from "next/navigation";
+import {Button, Typography} from "antd";
+import {ProLayout} from "@ant-design/pro-components";
+import {EyeOutlined, LikeOutlined, StarOutlined} from "@ant-design/icons";
+import color from "@wangeditor/basic-modules/dist/basic-modules/src/modules/color";
+
+function Page(){
+ const [data, setData] = useState()
+ const [isLoading, setLoading] = useState(false)
+ const params = useSearchParams()
+ const artID = params?.get("id")!
+ debugger
+ useEffect(() => {
+ setLoading(true)
+ axios.get('/articles/pub/'+artID)
+ .then((res) => res.data)
+ .then((data) => {
+ setData(data.data)
+ setLoading(false)
+ })
+ }, [artID])
+
+ if (isLoading) return Loading...
+ if (!data) return No data
+
+ const like = () => {
+ axios.post('/articles/pub/like', {
+ id: parseInt(artID),
+ like: !data.liked
+ })
+ .then((res) => res.data)
+ .then((res) => {
+ if(res.code == 0) {
+ if (data.liked) {
+ data.likeCnt --
+ } else {
+ data.likeCnt ++
+ }
+ data.liked = !data.liked
+ setData(Object.assign({}, data))
+ }
+ })
+ }
+
+ const collect = () => {
+ if (data.collected) {
+ return
+ }
+ axios.post('/articles/pub/collect', {
+ id: parseInt(artID),
+ // 你可以加上增删改查收藏夹的功能,在这里传入收藏夹 ID
+ cid: 0,
+ })
+ .then((res) => res.data)
+ .then((res) => {
+ if(res.code == 0) {
+ data.collectCnt ++
+ data.collected = !data.collected
+ setData(Object.assign({}, data))
+ }
+ })
+ }
+
+ return (
+
+
+
+ {data.title}
+
+
+
+
+
+ }> {data.readCnt}
+ }> {data.likeCnt}
+ }> {data.collectCnt}
+
+ )
+}
+
+export default Page
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/edit.tsx b/webook-fe/src/pages/users/edit.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4cfe1bb5fd51dda164f2a27a7cb98bcd269f8f2d
--- /dev/null
+++ b/webook-fe/src/pages/users/edit.tsx
@@ -0,0 +1,94 @@
+import React, {useEffect, useState} from 'react';
+import {Button, DatePicker, Form, Input} from 'antd';
+import axios from "@/axios/axios";
+import moment from 'moment';
+import router from "next/router";
+
+const { TextArea } = Input;
+
+const onFinish = (values: any) => {
+ if (values.birthday) {
+ values.birthday = moment(values.birthday).format("YYYY-MM-DD")
+ }
+ axios.post("/users/edit", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ if (res.data?.code == 0) {
+ router.push('/users/profile')
+ return
+ }
+ alert(res.data?.msg || "系统错误");
+ }).catch((err) => {
+ alert(err);
+ })
+};
+
+const onFinishFailed = (errorInfo: any) => {
+ alert("输入有误")
+};
+
+function EditForm() {
+ const p: Profile = {} as Profile
+ const [data, setData] = useState(p)
+ const [isLoading, setLoading] = useState(false)
+
+ useEffect(() => {
+ setLoading(true)
+ axios.get('/users/profile')
+ .then((res) => res.data)
+ .then((data) => {
+ setData(data)
+ setLoading(false)
+ })
+ }, [])
+
+ if (isLoading) return Loading...
+ if (!data) return No profile data
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+export default EditForm;
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/index.tsx b/webook-fe/src/pages/users/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fa0d328c997503279a58f3753248fc79b7209610
--- /dev/null
+++ b/webook-fe/src/pages/users/index.tsx
@@ -0,0 +1,15 @@
+import type { Metadata } from 'next'
+import {BrowserRouter, Route, Routes} from "react-router-dom";
+import React from "react";
+export const metadata: Metadata = {
+ title: '小微书',
+ description: '你的第一个 Web 应用',
+}
+
+const App = () => {
+ return
+ hello
+
+}
+
+export default App
diff --git a/webook-fe/src/pages/users/login.tsx b/webook-fe/src/pages/users/login.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e33ad4291bfb8a13abbaf232a51c77a2b880ba67
--- /dev/null
+++ b/webook-fe/src/pages/users/login.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { Button, Form, Input } from 'antd';
+import axios from "@/axios/axios";
+import Link from "next/link";
+import router from "next/router";
+
+const onFinish = (values: any) => {
+ axios.post("/users/login", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ alert(res.data)
+ router.push('/articles/list')
+ }).catch((err) => {
+ alert(err);
+ })
+};
+
+const onFinishFailed = (errorInfo: any) => {
+ alert("输入有误")
+};
+
+const LoginForm: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ 手机号登录
+
+
+ 微信扫码登录
+
+
+ 注册
+
+
+
+)};
+
+export default LoginForm;
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/login_sms.tsx b/webook-fe/src/pages/users/login_sms.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c2c58c200653ae9d7b221ed78c8250cef9d02c2e
--- /dev/null
+++ b/webook-fe/src/pages/users/login_sms.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { Button, Form, Input } from 'antd';
+import axios from "@/axios/axios";
+import router from "next/router";
+
+const onFinish = (values: any) => {
+ axios.post("/users/login_sms", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+
+ if (res.data.code == 0) {
+ router.push('/users/profile')
+ return;
+ }
+ alert(res.data.msg)
+ }).catch((err) => {
+ alert(err);
+ })
+};
+
+const onFinishFailed = (errorInfo: any) => {
+ alert("输入有误")
+};
+
+const LoginFormSMS: React.FC = () => {
+ const [form] = Form.useForm();
+
+ const sendCode = () => {
+ const data = form.getFieldValue("phone")
+ axios.post("/users/login_sms/code/send", {"phone": data} ).then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ alert(res?.data?.msg || "系统错误,请重试")
+ }).catch((err) => {
+ alert(err);
+ })
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)};
+
+export default LoginFormSMS;
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/login_wechat.tsx b/webook-fe/src/pages/users/login_wechat.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a2b0d1de905434332d7c8ca14a0b40b93e25abf4
--- /dev/null
+++ b/webook-fe/src/pages/users/login_wechat.tsx
@@ -0,0 +1,29 @@
+import React, { useState, useEffect } from 'react';
+import axios from "@/axios/axios";
+import {redirect} from "next/navigation";
+
+function Page() {
+ const [isLoading, setLoading] = useState(false)
+
+ useEffect(() => {
+ setLoading(true)
+ axios.get('/oauth2/wechat/authurl')
+ .then((res) => res.data)
+ .then((data) => {
+ setLoading(false)
+ if(data && data.data) {
+ window.location.href = data.data
+ }
+ })
+ }, [])
+
+ if (isLoading) return Loading...
+
+ return (
+
+
+
+ )
+}
+
+export default Page
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/model.tsx b/webook-fe/src/pages/users/model.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7782e964dd9055c7ef2e35353bf2f34b0c13ff65
--- /dev/null
+++ b/webook-fe/src/pages/users/model.tsx
@@ -0,0 +1,7 @@
+type Profile = {
+ Email: string
+ Phone: string
+ Nickname: string
+ Birthday: string
+ AboutMe: string
+}
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/profile.tsx b/webook-fe/src/pages/users/profile.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c6567f1860c5543f8bea799d82059b640b510989
--- /dev/null
+++ b/webook-fe/src/pages/users/profile.tsx
@@ -0,0 +1,61 @@
+import { ProDescriptions } from '@ant-design/pro-components';
+import React, { useState, useEffect } from 'react';
+import { Button } from 'antd';
+import axios from "@/axios/axios";
+
+function Page() {
+ let p: Profile = {Email: "", Phone: "", Nickname: "", Birthday:"", AboutMe: ""}
+ const [data, setData] = useState(p)
+ const [isLoading, setLoading] = useState(false)
+
+ useEffect(() => {
+ setLoading(true)
+ axios.get('/users/profile')
+ .then((res) => res.data)
+ .then((data) => {
+ setData(data)
+ setLoading(false)
+ })
+ }, [])
+
+ if (isLoading) return Loading...
+ if (!data) return No profile data
+
+ return (
+
+
+ {data.Nickname}
+
+ {data.Email}
+
+ {data.Phone}
+
+
+ {data.Birthday}
+
+
+ {data.AboutMe}
+
+
+
+
+
+
+ )
+}
+
+export default Page
\ No newline at end of file
diff --git a/webook-fe/src/pages/users/signup.tsx b/webook-fe/src/pages/users/signup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..02a07254f594566b982a8966b6a7d420bfd3dd43
--- /dev/null
+++ b/webook-fe/src/pages/users/signup.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Button, Form, Input } from 'antd';
+import axios from "@/axios/axios";
+import Link from "next/link";
+
+const onFinish = (values: any) => {
+ axios.post("/users/signup", values)
+ .then((res) => {
+ if(res.status != 200) {
+ alert(res.statusText);
+ return
+ }
+ alert(res.data);
+ }).catch((err) => {
+ alert(err);
+ })
+};
+
+const onFinishFailed = (errorInfo: any) => {
+ alert("输入有误")
+};
+
+const SignupForm: React.FC = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+);
+
+export default SignupForm;
\ No newline at end of file
diff --git a/webook-fe/src/typing.d.ts b/webook-fe/src/typing.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c128bcd2c79517a1463684b5f3ef04ef68b70aa3
--- /dev/null
+++ b/webook-fe/src/typing.d.ts
@@ -0,0 +1 @@
+declare const BACKEND_BASE_URL: 'http://localhost:8080';
\ No newline at end of file
diff --git a/webook-fe/tailwind.config.js b/webook-fe/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..d53b2eaa03b647a8ee06dfeb4ff60e6b2a9f41c9
--- /dev/null
+++ b/webook-fe/tailwind.config.js
@@ -0,0 +1,18 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+ 'gradient-conic':
+ 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/webook-fe/tsconfig.json b/webook-fe/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..0c7555fa765cc10e28b0de0573ecf7a099ebafeb
--- /dev/null
+++ b/webook-fe/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/webook/.DS_Store b/webook/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..972c57a8a7176b8de43102cfcb24788f18abdb49
Binary files /dev/null and b/webook/.DS_Store differ
diff --git a/webook/Dockerfile b/webook/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..348580bb39d75c2e1288da0f8092f14295f2fd63
--- /dev/null
+++ b/webook/Dockerfile
@@ -0,0 +1,10 @@
+# 基础镜像
+FROM ubuntu:20.04
+# 把编译后的打包进来这个镜像,放到工作目录 /app。你随便换
+COPY webook /app/webook
+WORKDIR /app
+# CMD 是执行命令
+# 最佳
+ENTRYPOINT ["/app/webook"]
+
+
diff --git a/webook/Makefile b/webook/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..6014b85e2914c946a6bcb575ccebed9bdcf83c4a
--- /dev/null
+++ b/webook/Makefile
@@ -0,0 +1,6 @@
+.PHONY: docker
+docker:
+ @rm webook || true
+ @GOOS=linux GOARCH=arm go build -tags=k8s -o webook .
+ @docker rmi -f flycash/webook:v0.0.1
+ @docker build -t flycash/webook-live:v0.0.1 .
\ No newline at end of file
diff --git a/webook/account/config/dev.yaml b/webook/account/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..213f349f54b10ff93a358d661d5ec5811dc6caed
--- /dev/null
+++ b/webook/account/config/dev.yaml
@@ -0,0 +1,11 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook_account"
+
+grpc:
+ server:
+ port: 8100
+ etcdTTL: 60
+
+etcd:
+ endpoints:
+ - "localhost:12379"
\ No newline at end of file
diff --git a/webook/account/domain/credit.go b/webook/account/domain/credit.go
new file mode 100644
index 0000000000000000000000000000000000000000..50d361fcc10f6bea972f88fc9505e52f067d47c2
--- /dev/null
+++ b/webook/account/domain/credit.go
@@ -0,0 +1,27 @@
+package domain
+
+type Credit struct {
+ Biz string
+ BizId int64
+ Items []CreditItem
+}
+
+type CreditItem struct {
+ Uid int64
+ Account int64
+ AccountType AccountType
+ Amt int64
+ Currency string
+}
+
+type AccountType uint8
+
+func (a AccountType) AsUint8() uint8 {
+ return uint8(a)
+}
+
+const (
+ AccountTypeUnknown = iota
+ AccountTypeReward
+ AccountTypeSystem
+)
diff --git a/webook/account/grpc/account.go b/webook/account/grpc/account.go
new file mode 100644
index 0000000000000000000000000000000000000000..4f9f494e5e724712f3d7c79e9ee514942fb18193
--- /dev/null
+++ b/webook/account/grpc/account.go
@@ -0,0 +1,50 @@
+package grpc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+ "gitee.com/geekbang/basic-go/webook/account/service"
+ accountv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/account/v1"
+ "github.com/ecodeclub/ekit/slice"
+ "google.golang.org/grpc"
+)
+
+type AccountServiceServer struct {
+ accountv1.UnimplementedAccountServiceServer
+ svc service.AccountService
+}
+
+func NewAccountServiceServer(svc service.AccountService) *AccountServiceServer {
+ return &AccountServiceServer{svc: svc}
+}
+
+func (a *AccountServiceServer) Credit(ctx context.Context,
+ req *accountv1.CreditRequest) (*accountv1.CreditResponse, error) {
+ err := a.svc.Credit(ctx, a.toDomain(req))
+ return &accountv1.CreditResponse{}, err
+}
+
+func (a *AccountServiceServer) toDomain(c *accountv1.CreditRequest) domain.Credit {
+ return domain.Credit{
+ Biz: c.Biz,
+ BizId: c.BizId,
+ Items: slice.Map(c.Items, func(idx int, src *accountv1.CreditItem) domain.CreditItem {
+ return a.itemToDomain(src)
+ }),
+ }
+}
+
+func (a *AccountServiceServer) itemToDomain(c *accountv1.CreditItem) domain.CreditItem {
+ return domain.CreditItem{
+ Account: c.Account,
+ Amt: c.Amt,
+ Uid: c.Uid,
+ // 两者取值都是一样的,我偷个懒,直接转
+ AccountType: domain.AccountType(c.AccountType),
+ Currency: c.Currency,
+ }
+}
+
+func (a *AccountServiceServer) Register(server *grpc.Server) {
+ accountv1.RegisterAccountServiceServer(server, a)
+}
diff --git a/webook/account/integration/server_test.go b/webook/account/integration/server_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e91b2abc852314f0b28513cbe02f5214a7796a0c
--- /dev/null
+++ b/webook/account/integration/server_test.go
@@ -0,0 +1,151 @@
+package integration
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/account/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ accountv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/account/v1"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+ "testing"
+ "time"
+)
+
+type AccountServiceServerTestSuite struct {
+ suite.Suite
+ db *gorm.DB
+ server *grpc.AccountServiceServer
+}
+
+func (s *AccountServiceServerTestSuite) SetupSuite() {
+ s.db = startup.InitTestDB()
+ s.server = startup.InitAccountService()
+}
+
+func (s *AccountServiceServerTestSuite) TearDownTest() {
+ s.db.Exec("TRUNCATE TABLE `accounts`")
+ //s.db.Exec("TRUNCATE TABLE `account_activities`")
+}
+
+func (s *AccountServiceServerTestSuite) TestCredit() {
+ testCases := []struct {
+ name string
+ before func(t *testing.T)
+ after func(t *testing.T)
+ req *accountv1.CreditRequest
+ wantErr error
+ }{
+ {
+ name: "用户账号不存在",
+ before: func(t *testing.T) {
+
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var sysAccount dao.Account
+ err := s.db.WithContext(ctx).Where("type = ?", uint8(accountv1.AccountType_AccountTypeSystem)).
+ First(&sysAccount).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(10), sysAccount.Balance)
+ var usrAccount dao.Account
+ err = s.db.WithContext(ctx).Where("uid = ?", 1024).
+ First(&usrAccount).Error
+ require.NoError(t, err)
+ usrAccount.Id = 0
+ assert.True(t, usrAccount.Ctime > 0)
+ usrAccount.Ctime = 0
+ assert.True(t, usrAccount.Utime > 0)
+ usrAccount.Utime = 0
+ assert.Equal(t, dao.Account{
+ Account: 123,
+ Uid: 1024,
+ Type: uint8(accountv1.AccountType_AccountTypeReward),
+ Balance: 100,
+ Currency: "CNY",
+ }, usrAccount)
+ },
+ req: &accountv1.CreditRequest{
+ Biz: "test",
+ BizId: 123,
+ Items: []*accountv1.CreditItem{
+ {
+ Account: 123,
+ AccountType: accountv1.AccountType_AccountTypeReward,
+ Amt: 100,
+ Currency: "CNY",
+ Uid: 1024,
+ },
+ {
+ AccountType: accountv1.AccountType_AccountTypeSystem,
+ Amt: 10,
+ Currency: "CNY",
+ },
+ },
+ },
+ },
+ {
+ name: "用户账号存在",
+ before: func(t *testing.T) {
+ err := s.db.Create(&dao.Account{
+ Uid: 1025,
+ Account: 123,
+ Type: uint8(accountv1.AccountType_AccountTypeReward),
+ Balance: 300,
+ Currency: "CNY",
+ Ctime: 1111,
+ Utime: 2222,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var usrAccount dao.Account
+ err := s.db.WithContext(ctx).Where("uid = ?", 1025).
+ First(&usrAccount).Error
+ require.NoError(t, err)
+ usrAccount.Id = 0
+ assert.True(t, usrAccount.Ctime > 0)
+ usrAccount.Ctime = 0
+ assert.True(t, usrAccount.Utime > 0)
+ usrAccount.Utime = 0
+ assert.Equal(t, dao.Account{
+ Account: 123,
+ Uid: 1025,
+ Type: uint8(accountv1.AccountType_AccountTypeReward),
+ Balance: 400,
+ Currency: "CNY",
+ }, usrAccount)
+ },
+ req: &accountv1.CreditRequest{
+ Biz: "test",
+ BizId: 123,
+ Items: []*accountv1.CreditItem{
+ {
+ Account: 123,
+ AccountType: accountv1.AccountType_AccountTypeReward,
+ Amt: 100,
+ Currency: "CNY",
+ Uid: 1025,
+ },
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ _, err := s.server.Credit(context.Background(), tc.req)
+ assert.Equal(t, tc.wantErr, err)
+ tc.after(t)
+ })
+ }
+}
+
+func TestAccountServiceServer(t *testing.T) {
+ suite.Run(t, new(AccountServiceServerTestSuite))
+}
diff --git a/webook/account/integration/startup/db.go b/webook/account/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..014d229bd27a987149ead0bfb9c768387d3a9f73
--- /dev/null
+++ b/webook/account/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook_account"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/account/integration/startup/wire.go b/webook/account/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..2406d83b2db6890a0fbd9ef23e9b00954f85ddf3
--- /dev/null
+++ b/webook/account/integration/startup/wire.go
@@ -0,0 +1,20 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/account/repository"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/account/service"
+ "github.com/google/wire"
+)
+
+func InitAccountService() *grpc.AccountServiceServer {
+ wire.Build(InitTestDB,
+ dao.NewCreditGORMDAO,
+ repository.NewAccountRepository,
+ service.NewAccountService,
+ grpc.NewAccountServiceServer)
+ return new(grpc.AccountServiceServer)
+}
diff --git a/webook/account/integration/startup/wire_gen.go b/webook/account/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..26173dc07fe6099fa40d64338416c7328b063381
--- /dev/null
+++ b/webook/account/integration/startup/wire_gen.go
@@ -0,0 +1,25 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/account/repository"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/account/service"
+)
+
+// Injectors from wire.go:
+
+func InitAccountService() *grpc.AccountServiceServer {
+ gormDB := InitTestDB()
+ accountDAO := dao.NewCreditGORMDAO(gormDB)
+ accountRepository := repository.NewAccountRepository(accountDAO)
+ accountService := service.NewAccountService(accountRepository)
+ accountServiceServer := grpc.NewAccountServiceServer(accountService)
+ return accountServiceServer
+}
diff --git a/webook/account/ioc/db.go b/webook/account/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..5613c5c164127c79b84a250b40dbbd19eb02ab6b
--- /dev/null
+++ b/webook/account/ioc/db.go
@@ -0,0 +1,31 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func InitDB() *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v1, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{})
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
diff --git a/webook/account/ioc/etcd.go b/webook/account/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/account/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/account/ioc/grpc.go b/webook/account/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..8d511ac7a551a69f84954d7351660c4c35708cac
--- /dev/null
+++ b/webook/account/ioc/grpc.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ grpc3 "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(asc *grpc3.AccountServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ asc.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "reward",
+ L: l,
+ EtcdClient: ecli,
+ EtcdTTL: cfg.EtcdTTL,
+ }
+}
diff --git a/webook/account/ioc/log.go b/webook/account/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/account/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/account/main.go b/webook/account/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..f41594ed4c86d2886b27d0a8e469a29537c032f6
--- /dev/null
+++ b/webook/account/main.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ err := app.GRPCServer.Serve()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/webook/account/repository/account_repo.go b/webook/account/repository/account_repo.go
new file mode 100644
index 0000000000000000000000000000000000000000..2cbde52953b56b72df953fb8b99f417207fc2c86
--- /dev/null
+++ b/webook/account/repository/account_repo.go
@@ -0,0 +1,36 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "time"
+)
+
+type accountRepository struct {
+ dao dao.AccountDAO
+}
+
+func NewAccountRepository(dao dao.AccountDAO) AccountRepository {
+ return &accountRepository{dao: dao}
+}
+
+func (a *accountRepository) AddCredit(ctx context.Context, c domain.Credit) error {
+ activities := make([]dao.AccountActivity, 0, len(c.Items))
+ now := time.Now().UnixMilli()
+ for _, itm := range c.Items {
+ activities = append(activities, dao.AccountActivity{
+ Uid: itm.Uid,
+ Biz: c.Biz,
+ BizId: c.BizId,
+ Account: itm.Account,
+ AccountType: itm.AccountType.AsUint8(),
+ Amount: itm.Amt,
+ Currency: itm.Currency,
+ Ctime: now,
+ Utime: now,
+ })
+ }
+ // 把它改成了记录账号变动活动,同时会去更新余额
+ return a.dao.AddActivities(ctx, activities...)
+}
diff --git a/webook/account/repository/dao/gorm.go b/webook/account/repository/dao/gorm.go
new file mode 100644
index 0000000000000000000000000000000000000000..82dae1f8ab9b68b0883cb3d73720e404ef17a3bc
--- /dev/null
+++ b/webook/account/repository/dao/gorm.go
@@ -0,0 +1,51 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "time"
+)
+
+type AccountGORMDAO struct {
+ db *gorm.DB
+}
+
+func NewCreditGORMDAO(db *gorm.DB) AccountDAO {
+ return &AccountGORMDAO{db: db}
+}
+
+// AddActivities 一次业务里面的相关账号的余额变动
+func (c *AccountGORMDAO) AddActivities(ctx context.Context, activities ...AccountActivity) error {
+ // 这里应该是一个事务
+ // 同一个业务,牵涉到了多个账号,你必然是要求,要么全部成功,要么全部失败,不然就会出于中间状态
+ return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ // 修改余额
+ // 添加支付记录
+ now := time.Now().UnixMilli()
+ for _, act := range activities {
+ // 正常来说,你在一个平台注册的时候,
+ // 后面的这些支撑系统,都会提前给你准备好账号
+ err := tx.Create(&Account{
+ Uid: act.Uid,
+ Account: act.Account,
+ Type: act.AccountType,
+ Balance: act.Account,
+ Currency: act.Currency,
+ Ctime: now,
+ Utime: now,
+ }).Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ // 记账,如果是减少呢?
+ "balance": gorm.Expr("`balance` + ?", act.Amount),
+ "utime": now,
+ }),
+ }).Error
+ if err != nil {
+ return err
+ }
+ }
+ // 批量插入
+ return tx.Create(activities).Error
+ })
+}
diff --git a/webook/account/repository/dao/init.go b/webook/account/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..ce654eae7c4d47c45de2ef8ec12dc778ab576307
--- /dev/null
+++ b/webook/account/repository/dao/init.go
@@ -0,0 +1,25 @@
+package dao
+
+import (
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+ "gorm.io/gorm"
+ "time"
+)
+
+func InitTables(db *gorm.DB) error {
+ err := db.AutoMigrate(&Account{}, &AccountActivity{})
+ if err != nil {
+ return err
+ }
+ // 为了测试和调试方便,这里我补充一个初始化系统账号的代码
+ // 你在现实中是不需要的
+ now := time.Now().UnixMilli()
+ // 忽略这个错误,因为我在测试的反复运行了
+ _ = db.Create(&Account{
+ Type: domain.AccountTypeSystem,
+ Currency: "CNY",
+ Ctime: now,
+ Utime: now,
+ }).Error
+ return nil
+}
diff --git a/webook/account/repository/dao/types.go b/webook/account/repository/dao/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6944ba99ce9a945c400eea4f616d0e5435b0319
--- /dev/null
+++ b/webook/account/repository/dao/types.go
@@ -0,0 +1,64 @@
+package dao
+
+import "context"
+
+type AccountDAO interface {
+ AddActivities(ctx context.Context, activities ...AccountActivity) error
+}
+
+// Account 账号本体
+type Account struct {
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ // 对应的用户的 ID,如果是系统账号,它是 0,或者你定义 sql.NullInt64
+ Uid int64 `gorm:"uniqueIndex:account_uid"`
+ // 账号 ID,这个才是对外使用的,有些情况下,这个字段会被设计成 string
+ // 123_RMB
+ Account int64 `gorm:"uniqueIndex:account_uid"`
+ // 一个人可能有很多账号,你在这里可以用于区分
+ Type uint8 `gorm:"uniqueIndex:account_uid"`
+
+ // 账号本身可以有很多额外的字段
+ // 例如跟会计有关的,跟税务有关的,跟洗钱有关的
+ // 跟审计有关的,跟安全有关的
+
+ // 可用余额
+ // 一般来说,一种货币就一个账号,比较好处理(个人认为)
+ // 有些一个账号,但是支持多种货币,那么就需要关联另外一张表。
+ // 记录每一个币种的余额
+ Balance int64
+ Currency string
+
+ Ctime int64
+ Utime int64
+}
+
+//type AccountBalance struct {
+// Aid int64
+// Balance int64
+// Currency string
+//}
+
+type AccountActivity struct {
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ Uid int64 `gorm:"index:account_uid"`
+ // 这边有些设计会只用一个单独的 txn_id 来标记
+ // 加上这些 业务 ID,DEBUG 的时候贼好用
+ Biz string
+ BizId int64
+ // account 账号
+ Account int64 `gorm:"index:account_uid"`
+ AccountType uint8 `gorm:"index:account_uid"`
+ // 调整的金额,有些设计不想引入负数,就会增加一个类型
+ // 标记是增加还是减少,暂时我们还不需要
+ Amount int64
+ Currency string
+
+ // Type Credit 代表 +,Debit 代表 -
+
+ Ctime int64
+ Utime int64
+}
+
+func (AccountActivity) TableName() string {
+ return "account_activities"
+}
diff --git a/webook/account/repository/types.go b/webook/account/repository/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c52e5b56543e8663f60a917e9b32f4f49a63d13
--- /dev/null
+++ b/webook/account/repository/types.go
@@ -0,0 +1,10 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+)
+
+type AccountRepository interface {
+ AddCredit(ctx context.Context, c domain.Credit) error
+}
diff --git a/webook/account/service/account.go b/webook/account/service/account.go
new file mode 100644
index 0000000000000000000000000000000000000000..53ff12dcb5c850f2956261bc0ac7c808b5328151
--- /dev/null
+++ b/webook/account/service/account.go
@@ -0,0 +1,21 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+ "gitee.com/geekbang/basic-go/webook/account/repository"
+)
+
+type accountService struct {
+ repo repository.AccountRepository
+}
+
+func NewAccountService(repo repository.AccountRepository) AccountService {
+ return &accountService{repo: repo}
+}
+
+func (a *accountService) Credit(ctx context.Context, cr domain.Credit) error {
+ // redis 里面看一下有没有这个 biz + biz_id,有就认为已经处理过了
+ // 但是最终肯定是利用唯一索引来兜底的
+ return a.repo.AddCredit(ctx, cr)
+}
diff --git a/webook/account/service/types.go b/webook/account/service/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f339503fd7f14628ffc900c510576b1e1b8b4c6
--- /dev/null
+++ b/webook/account/service/types.go
@@ -0,0 +1,10 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/account/domain"
+)
+
+type AccountService interface {
+ Credit(ctx context.Context, cr domain.Credit) error
+}
diff --git a/webook/account/wire.go b/webook/account/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..c31b2c3c4b24ad72ec8b592d637972e1a5555d1e
--- /dev/null
+++ b/webook/account/wire.go
@@ -0,0 +1,27 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/account/ioc"
+ "gitee.com/geekbang/basic-go/webook/account/repository"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/account/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+ "github.com/google/wire"
+)
+
+func Init() *wego.App {
+ wire.Build(
+ ioc.InitDB,
+ ioc.InitLogger,
+ ioc.InitEtcdClient,
+ ioc.InitGRPCxServer,
+ dao.NewCreditGORMDAO,
+ repository.NewAccountRepository,
+ service.NewAccountService,
+ grpc.NewAccountServiceServer,
+ wire.Struct(new(wego.App), "GRPCServer"))
+ return new(wego.App)
+}
diff --git a/webook/account/wire_gen.go b/webook/account/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..45950e850f782331f5c8646119636916a3610ba2
--- /dev/null
+++ b/webook/account/wire_gen.go
@@ -0,0 +1,33 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/account/grpc"
+ "gitee.com/geekbang/basic-go/webook/account/ioc"
+ "gitee.com/geekbang/basic-go/webook/account/repository"
+ "gitee.com/geekbang/basic-go/webook/account/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/account/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+)
+
+// Injectors from wire.go:
+
+func Init() *wego.App {
+ db := ioc.InitDB()
+ accountDAO := dao.NewCreditGORMDAO(db)
+ accountRepository := repository.NewAccountRepository(accountDAO)
+ accountService := service.NewAccountService(accountRepository)
+ accountServiceServer := grpc.NewAccountServiceServer(accountService)
+ client := ioc.InitEtcdClient()
+ loggerV1 := ioc.InitLogger()
+ server := ioc.InitGRPCxServer(accountServiceServer, client, loggerV1)
+ app := &wego.App{
+ GRPCServer: server,
+ }
+ return app
+}
diff --git a/webook/api/http/swagger.yaml b/webook/api/http/swagger.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/webook/api/proto/account/v1/account.proto b/webook/api/proto/account/v1/account.proto
new file mode 100644
index 0000000000000000000000000000000000000000..41ccd157f8a9d524f36dc777556d6018294ad8a3
--- /dev/null
+++ b/webook/api/proto/account/v1/account.proto
@@ -0,0 +1,44 @@
+syntax = "proto3";
+
+package account.v1;
+option go_package="account/v1;accountv1";
+
+service AccountService {
+ // 入账
+ rpc Credit(CreditRequest) returns(CreditResponse);
+}
+
+message CreditRequest {
+ // 具体入账的各种信息
+ // 有一些设计师不用这个业务方的凭证,而是用支付的凭证
+ string biz = 1;
+ int64 biz_id = 2;
+ // 这里你可以有更多的字段,也就是之前我说的,订单的详情,支付的详情,你都可以在这里加
+ repeated CreditItem items = 3;
+}
+
+message CreditResponse {
+}
+
+message CreditItem {
+ // 记录哪一方收到了多少钱
+ // 怎么用字段表示每一方
+ // 平台账号你怎么表达?
+ int64 account = 1;
+ // 不同账号用在不同的场景里面,用一个 type 来表示会更好
+ AccountType account_type = 2;
+
+ int64 amt = 3;
+ string currency = 4;
+
+ int64 uid = 5;
+
+}
+
+enum AccountType {
+ AccountTypeUnknown = 0;
+ // 个人赞赏账号
+ AccountTypeReward = 1;
+ // 平台分成账号
+ AccountTypeSystem = 2;
+}
\ No newline at end of file
diff --git a/webook/api/proto/comment/v1/comment.proto b/webook/api/proto/comment/v1/comment.proto
new file mode 100644
index 0000000000000000000000000000000000000000..2638a8c21aa0c16e33ef1d5794c3744d26cc36a2
--- /dev/null
+++ b/webook/api/proto/comment/v1/comment.proto
@@ -0,0 +1,76 @@
+syntax = "proto3";
+
+package comment.v1;
+option go_package="comment/v1;commentv1";
+
+import "google/protobuf/timestamp.proto";
+
+
+service CommentService {
+ // GetCommentList Comment的id为0 获取一级评论
+ rpc GetCommentList (CommentListRequest) returns (CommentListResponse);
+
+ // DeleteComment 删除评论,删除本评论和其子评论
+ rpc DeleteComment (DeleteCommentRequest) returns (DeleteCommentResponse);
+
+ // CreateComment 创建评论
+ rpc CreateComment (CreateCommentRequest) returns (CreateCommentResponse);
+
+ // 直接评论和回复分开
+// rpc CommentDirectly();
+// rpc Reply();
+
+ rpc GetMoreReplies(GetMoreRepliesRequest) returns (GetMoreRepliesResponse);
+}
+
+// 安排评论时间排序,在使用自增主键的情况下,实际上就是按照主键大小排序,倒序
+//
+message CommentListRequest {
+ string biz = 1;
+ int64 bizid = 2;
+ int64 min_id = 3;
+ int64 limit = 4;
+}
+
+message CommentListResponse {
+ repeated Comment comments = 1;
+}
+
+message DeleteCommentRequest {
+ int64 id = 1;
+}
+
+message DeleteCommentResponse {
+}
+
+message CreateCommentRequest {
+ Comment comment = 1;
+}
+
+message CreateCommentResponse {
+}
+
+message GetMoreRepliesRequest {
+ int64 rid = 1;
+ int64 max_id = 2;
+ int64 limit = 3;
+}
+message GetMoreRepliesResponse {
+ repeated Comment replies = 1;
+}
+
+message Comment {
+ int64 id = 1;
+ int64 uid = 2;
+ string biz = 3;
+ int64 bizid = 4;
+ string content = 5;
+// 这里你可以考虑,只传入 id
+ Comment root_comment = 6;
+ // 只传入 id
+ Comment parent_comment = 7;
+ // 正常来说,你在时间传递上,如果不想用 int64 之类的
+ // 就可以考虑使用这个 Timestamp
+ google.protobuf.Timestamp ctime = 9;
+ google.protobuf.Timestamp utime = 10;
+}
diff --git a/webook/api/proto/feed/v1/feed.proto b/webook/api/proto/feed/v1/feed.proto
new file mode 100644
index 0000000000000000000000000000000000000000..713df03e77dbe5f98f6b4b6724858786f5ebe28c
--- /dev/null
+++ b/webook/api/proto/feed/v1/feed.proto
@@ -0,0 +1,48 @@
+syntax = "proto3";
+
+package feed.v1;
+
+option go_package="feed/v1;feedv1";
+
+message User {
+ int64 id = 1;
+}
+
+message Article {
+ int64 id = 1;
+}
+
+message FeedEvent {
+ int64 id = 1;
+ User user = 2;
+ string type = 3;
+ string content = 4;
+ int64 ctime = 5;
+}
+
+message FeedEventV1 {
+ string type = 1;
+ map metadata = 2;
+}
+
+
+service FeedSvc {
+ rpc CreateFeedEvent(CreateFeedEventRequest) returns (CreateFeedEventResponse);
+ rpc FindFeedEvents( FindFeedEventsRequest)returns (FindFeedEventsResponse);
+}
+
+message CreateFeedEventRequest {
+ FeedEvent feedEvent = 1;
+}
+
+message CreateFeedEventResponse{
+}
+
+message FindFeedEventsRequest {
+ int64 Uid = 1;
+ int64 Limit = 2;
+ int64 timestamp = 3;
+}
+message FindFeedEventsResponse {
+ repeated FeedEvent feedEvents = 1;
+}
\ No newline at end of file
diff --git a/webook/api/proto/follow/v1/follow.proto b/webook/api/proto/follow/v1/follow.proto
new file mode 100644
index 0000000000000000000000000000000000000000..f8107a8b292a1e8666e6b7cd4c97a573b1d809c9
--- /dev/null
+++ b/webook/api/proto/follow/v1/follow.proto
@@ -0,0 +1,102 @@
+syntax = "proto3";
+
+package follow.v1;
+option go_package="follow/v1;followv1";
+
+
+message FollowRelation {
+ int64 id = 1;
+ int64 follower = 2;
+ int64 followee = 3;
+}
+
+message FollowStatic {
+ // 被多少人关注
+ int64 followers = 1;
+ // 自己关注了多少人
+ int64 followees = 2;
+}
+
+service FollowService {
+ // 增删
+ rpc Follow (FollowRequest) returns (FollowResponse);
+ rpc CancelFollow(CancelFollowRequest) returns (CancelFollowResponse);
+
+ // 改,例如说你准备支持备注、标签类的,那么就会有对应的修改功能
+
+ // 获得某个人的关注列表
+ rpc GetFollowee (GetFolloweeRequest) returns (GetFolloweeResponse);
+ // 获得某个人关注另外一个人的详细信息
+ rpc FollowInfo (FollowInfoRequest) returns (FollowInfoResponse);
+ // 获取某人的粉丝列表
+ rpc GetFollower (GetFollowerRequest)returns(GetFollowerResponse );
+ // 获取默认的关注人数
+ rpc GetFollowStatic(GetFollowStaticRequest)returns(GetFollowStaticResponse);
+}
+message GetFollowStaticRequest{
+ int64 followee = 1;
+}
+
+message GetFollowStaticResponse{
+ FollowStatic followStatic = 1;
+}
+message GetFolloweeRequest {
+ // 关注者,也就是某人查看自己的关注列表
+ int64 follower = 1;
+ // min_id, max_id
+ int64 offset = 2;
+ int64 limit =3;
+}
+
+message GetFolloweeResponse {
+ repeated FollowRelation follow_relations = 1;
+}
+
+message FollowInfoRequest {
+ // 关注者
+ int64 follower = 1;
+ // 被关注者
+ int64 followee = 2;
+}
+
+message FollowInfoResponse {
+ FollowRelation follow_relation = 1;
+}
+
+message FollowRequest {
+ // 被关注者
+ int64 followee = 1;
+ // 关注者
+ int64 follower = 2;
+ // 如果说你有额外的功能
+ // 分组功能
+// int64 gid = 3;
+ // 标签功能
+// repeated int64 label_ids = 4;
+ // 比如说是否主动提醒 follower,followee 有了新动态
+// bool notification = 5;
+}
+
+message FollowResponse {
+}
+
+message CancelFollowRequest {
+ // 这里是不是可以考虑传主键
+
+
+ // 被关注者
+ int64 followee = 1;
+ // 关注者
+ int64 follower = 2;
+}
+
+message CancelFollowResponse {
+}
+
+
+message GetFollowerRequest {
+ int64 followee = 1;
+}
+message GetFollowerResponse {
+ repeated FollowRelation follow_relations = 1;
+}
\ No newline at end of file
diff --git a/webook/api/proto/gen/account/v1/account.pb.go b/webook/api/proto/gen/account/v1/account.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..aa8b8da47668e87f8a50afa22f2f5dc46647a819
--- /dev/null
+++ b/webook/api/proto/gen/account/v1/account.pb.go
@@ -0,0 +1,403 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: account/v1/account.proto
+
+package accountv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type AccountType int32
+
+const (
+ AccountType_AccountTypeUnknown AccountType = 0
+ // 个人赞赏账号
+ AccountType_AccountTypeReward AccountType = 1
+ // 平台分成账号
+ AccountType_AccountTypeSystem AccountType = 2
+)
+
+// Enum value maps for AccountType.
+var (
+ AccountType_name = map[int32]string{
+ 0: "AccountTypeUnknown",
+ 1: "AccountTypeReward",
+ 2: "AccountTypeSystem",
+ }
+ AccountType_value = map[string]int32{
+ "AccountTypeUnknown": 0,
+ "AccountTypeReward": 1,
+ "AccountTypeSystem": 2,
+ }
+)
+
+func (x AccountType) Enum() *AccountType {
+ p := new(AccountType)
+ *p = x
+ return p
+}
+
+func (x AccountType) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (AccountType) Descriptor() protoreflect.EnumDescriptor {
+ return file_account_v1_account_proto_enumTypes[0].Descriptor()
+}
+
+func (AccountType) Type() protoreflect.EnumType {
+ return &file_account_v1_account_proto_enumTypes[0]
+}
+
+func (x AccountType) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use AccountType.Descriptor instead.
+func (AccountType) EnumDescriptor() ([]byte, []int) {
+ return file_account_v1_account_proto_rawDescGZIP(), []int{0}
+}
+
+type CreditRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 具体入账的各种信息
+ // 有一些设计师不用这个业务方的凭证,而是用支付的凭证
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ // 这里你可以有更多的字段,也就是之前我说的,订单的详情,支付的详情,你都可以在这里加
+ Items []*CreditItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
+}
+
+func (x *CreditRequest) Reset() {
+ *x = CreditRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_account_v1_account_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreditRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreditRequest) ProtoMessage() {}
+
+func (x *CreditRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_account_v1_account_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreditRequest.ProtoReflect.Descriptor instead.
+func (*CreditRequest) Descriptor() ([]byte, []int) {
+ return file_account_v1_account_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CreditRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CreditRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *CreditRequest) GetItems() []*CreditItem {
+ if x != nil {
+ return x.Items
+ }
+ return nil
+}
+
+type CreditResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CreditResponse) Reset() {
+ *x = CreditResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_account_v1_account_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreditResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreditResponse) ProtoMessage() {}
+
+func (x *CreditResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_account_v1_account_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreditResponse.ProtoReflect.Descriptor instead.
+func (*CreditResponse) Descriptor() ([]byte, []int) {
+ return file_account_v1_account_proto_rawDescGZIP(), []int{1}
+}
+
+type CreditItem struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 记录哪一方收到了多少钱
+ // 怎么用字段表示每一方
+ // 平台账号你怎么表达?
+ Account int64 `protobuf:"varint,1,opt,name=account,proto3" json:"account,omitempty"`
+ // 不同账号用在不同的场景里面,用一个 type 来表示会更好
+ AccountType AccountType `protobuf:"varint,2,opt,name=account_type,json=accountType,proto3,enum=account.v1.AccountType" json:"account_type,omitempty"`
+ Amt int64 `protobuf:"varint,3,opt,name=amt,proto3" json:"amt,omitempty"`
+ Currency string `protobuf:"bytes,4,opt,name=currency,proto3" json:"currency,omitempty"`
+ Uid int64 `protobuf:"varint,5,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *CreditItem) Reset() {
+ *x = CreditItem{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_account_v1_account_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreditItem) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreditItem) ProtoMessage() {}
+
+func (x *CreditItem) ProtoReflect() protoreflect.Message {
+ mi := &file_account_v1_account_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreditItem.ProtoReflect.Descriptor instead.
+func (*CreditItem) Descriptor() ([]byte, []int) {
+ return file_account_v1_account_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *CreditItem) GetAccount() int64 {
+ if x != nil {
+ return x.Account
+ }
+ return 0
+}
+
+func (x *CreditItem) GetAccountType() AccountType {
+ if x != nil {
+ return x.AccountType
+ }
+ return AccountType_AccountTypeUnknown
+}
+
+func (x *CreditItem) GetAmt() int64 {
+ if x != nil {
+ return x.Amt
+ }
+ return 0
+}
+
+func (x *CreditItem) GetCurrency() string {
+ if x != nil {
+ return x.Currency
+ }
+ return ""
+}
+
+func (x *CreditItem) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+var File_account_v1_account_proto protoreflect.FileDescriptor
+
+var file_account_v1_account_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x61, 0x63, 0x63, 0x6f,
+ 0x75, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x22, 0x66, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64,
+ 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x16, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65,
+ 0x64, 0x69, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x10,
+ 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x22, 0xa2, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12,
+ 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0c, 0x61, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
+ 0x17, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65,
+ 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65,
+ 0x6e, 0x63, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x03, 0x75, 0x69, 0x64, 0x2a, 0x53, 0x0a, 0x0b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
+ 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54,
+ 0x79, 0x70, 0x65, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11,
+ 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72,
+ 0x64, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79,
+ 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x10, 0x02, 0x32, 0x51, 0x0a, 0x0e, 0x41, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3f, 0x0a, 0x06,
+ 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x12, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
+ 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43,
+ 0x72, 0x65, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xae, 0x01,
+ 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x76, 0x31,
+ 0x42, 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
+ 0x5a, 0x45, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b,
+ 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65,
+ 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67,
+ 0x65, 0x6e, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0a,
+ 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0a, 0x41, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x16, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+ 0xea, 0x02, 0x0b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_account_v1_account_proto_rawDescOnce sync.Once
+ file_account_v1_account_proto_rawDescData = file_account_v1_account_proto_rawDesc
+)
+
+func file_account_v1_account_proto_rawDescGZIP() []byte {
+ file_account_v1_account_proto_rawDescOnce.Do(func() {
+ file_account_v1_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_account_v1_account_proto_rawDescData)
+ })
+ return file_account_v1_account_proto_rawDescData
+}
+
+var file_account_v1_account_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_account_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_account_v1_account_proto_goTypes = []interface{}{
+ (AccountType)(0), // 0: account.v1.AccountType
+ (*CreditRequest)(nil), // 1: account.v1.CreditRequest
+ (*CreditResponse)(nil), // 2: account.v1.CreditResponse
+ (*CreditItem)(nil), // 3: account.v1.CreditItem
+}
+var file_account_v1_account_proto_depIdxs = []int32{
+ 3, // 0: account.v1.CreditRequest.items:type_name -> account.v1.CreditItem
+ 0, // 1: account.v1.CreditItem.account_type:type_name -> account.v1.AccountType
+ 1, // 2: account.v1.AccountService.Credit:input_type -> account.v1.CreditRequest
+ 2, // 3: account.v1.AccountService.Credit:output_type -> account.v1.CreditResponse
+ 3, // [3:4] is the sub-list for method output_type
+ 2, // [2:3] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_account_v1_account_proto_init() }
+func file_account_v1_account_proto_init() {
+ if File_account_v1_account_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_account_v1_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreditRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_account_v1_account_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreditResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_account_v1_account_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreditItem); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_account_v1_account_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 3,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_account_v1_account_proto_goTypes,
+ DependencyIndexes: file_account_v1_account_proto_depIdxs,
+ EnumInfos: file_account_v1_account_proto_enumTypes,
+ MessageInfos: file_account_v1_account_proto_msgTypes,
+ }.Build()
+ File_account_v1_account_proto = out.File
+ file_account_v1_account_proto_rawDesc = nil
+ file_account_v1_account_proto_goTypes = nil
+ file_account_v1_account_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/account/v1/account_grpc.pb.go b/webook/api/proto/gen/account/v1/account_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..51ffe946a665cf9335f711df8d371eeb1462757a
--- /dev/null
+++ b/webook/api/proto/gen/account/v1/account_grpc.pb.go
@@ -0,0 +1,111 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: account/v1/account.proto
+
+package accountv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ AccountService_Credit_FullMethodName = "/account.v1.AccountService/Credit"
+)
+
+// AccountServiceClient is the client API for AccountService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type AccountServiceClient interface {
+ // 入账
+ Credit(ctx context.Context, in *CreditRequest, opts ...grpc.CallOption) (*CreditResponse, error)
+}
+
+type accountServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewAccountServiceClient(cc grpc.ClientConnInterface) AccountServiceClient {
+ return &accountServiceClient{cc}
+}
+
+func (c *accountServiceClient) Credit(ctx context.Context, in *CreditRequest, opts ...grpc.CallOption) (*CreditResponse, error) {
+ out := new(CreditResponse)
+ err := c.cc.Invoke(ctx, AccountService_Credit_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// AccountServiceServer is the server API for AccountService service.
+// All implementations must embed UnimplementedAccountServiceServer
+// for forward compatibility
+type AccountServiceServer interface {
+ // 入账
+ Credit(context.Context, *CreditRequest) (*CreditResponse, error)
+ mustEmbedUnimplementedAccountServiceServer()
+}
+
+// UnimplementedAccountServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedAccountServiceServer struct {
+}
+
+func (UnimplementedAccountServiceServer) Credit(context.Context, *CreditRequest) (*CreditResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Credit not implemented")
+}
+func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {}
+
+// UnsafeAccountServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to AccountServiceServer will
+// result in compilation errors.
+type UnsafeAccountServiceServer interface {
+ mustEmbedUnimplementedAccountServiceServer()
+}
+
+func RegisterAccountServiceServer(s grpc.ServiceRegistrar, srv AccountServiceServer) {
+ s.RegisterService(&AccountService_ServiceDesc, srv)
+}
+
+func _AccountService_Credit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreditRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccountServiceServer).Credit(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: AccountService_Credit_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccountServiceServer).Credit(ctx, req.(*CreditRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var AccountService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "account.v1.AccountService",
+ HandlerType: (*AccountServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Credit",
+ Handler: _AccountService_Credit_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "account/v1/account.proto",
+}
diff --git a/webook/api/proto/gen/article/v1/article.pb.go b/webook/api/proto/gen/article/v1/article.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..b08401b0b4edf8bd0a67cfddcbfb8fcce96d8f3f
--- /dev/null
+++ b/webook/api/proto/gen/article/v1/article.pb.go
@@ -0,0 +1,1413 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: article/v1/article.proto
+
+package articlev1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Author struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 添加其他作者相关字段
+}
+
+func (x *Author) Reset() {
+ *x = Author{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Author) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Author) ProtoMessage() {}
+
+func (x *Author) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Author.ProtoReflect.Descriptor instead.
+func (*Author) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Author) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Author) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type Article struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+ Status int32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"`
+ Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+ Author *Author `protobuf:"bytes,5,opt,name=author,proto3" json:"author,omitempty"`
+ Ctime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=ctime,proto3" json:"ctime,omitempty"`
+ Utime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=utime,proto3" json:"utime,omitempty"`
+ Abstract string `protobuf:"bytes,8,opt,name=abstract,proto3" json:"abstract,omitempty"`
+}
+
+func (x *Article) Reset() {
+ *x = Article{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Article) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *Article) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Article) GetTitle() string {
+ if x != nil {
+ return x.Title
+ }
+ return ""
+}
+
+func (x *Article) GetStatus() int32 {
+ if x != nil {
+ return x.Status
+ }
+ return 0
+}
+
+func (x *Article) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *Article) GetAuthor() *Author {
+ if x != nil {
+ return x.Author
+ }
+ return nil
+}
+
+func (x *Article) GetCtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Ctime
+ }
+ return nil
+}
+
+func (x *Article) GetUtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Utime
+ }
+ return nil
+}
+
+func (x *Article) GetAbstract() string {
+ if x != nil {
+ return x.Abstract
+ }
+ return ""
+}
+
+type SaveRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *SaveRequest) Reset() {
+ *x = SaveRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SaveRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SaveRequest) ProtoMessage() {}
+
+func (x *SaveRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SaveRequest.ProtoReflect.Descriptor instead.
+func (*SaveRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SaveRequest) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type SaveResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *SaveResponse) Reset() {
+ *x = SaveResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SaveResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SaveResponse) ProtoMessage() {}
+
+func (x *SaveResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SaveResponse.ProtoReflect.Descriptor instead.
+func (*SaveResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SaveResponse) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type PublishRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *PublishRequest) Reset() {
+ *x = PublishRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PublishRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishRequest) ProtoMessage() {}
+
+func (x *PublishRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishRequest.ProtoReflect.Descriptor instead.
+func (*PublishRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *PublishRequest) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type PublishResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *PublishResponse) Reset() {
+ *x = PublishResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PublishResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishResponse) ProtoMessage() {}
+
+func (x *PublishResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishResponse.ProtoReflect.Descriptor instead.
+func (*PublishResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *PublishResponse) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type WithdrawRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"`
+ Id int64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *WithdrawRequest) Reset() {
+ *x = WithdrawRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *WithdrawRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WithdrawRequest) ProtoMessage() {}
+
+func (x *WithdrawRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use WithdrawRequest.ProtoReflect.Descriptor instead.
+func (*WithdrawRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *WithdrawRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *WithdrawRequest) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type WithdrawResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *WithdrawResponse) Reset() {
+ *x = WithdrawResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *WithdrawResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WithdrawResponse) ProtoMessage() {}
+
+func (x *WithdrawResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use WithdrawResponse.ProtoReflect.Descriptor instead.
+func (*WithdrawResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{7}
+}
+
+type PublishV1Request struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *PublishV1Request) Reset() {
+ *x = PublishV1Request{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PublishV1Request) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishV1Request) ProtoMessage() {}
+
+func (x *PublishV1Request) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishV1Request.ProtoReflect.Descriptor instead.
+func (*PublishV1Request) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *PublishV1Request) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type PublishV1Response struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *PublishV1Response) Reset() {
+ *x = PublishV1Response{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PublishV1Response) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishV1Response) ProtoMessage() {}
+
+func (x *PublishV1Response) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishV1Response.ProtoReflect.Descriptor instead.
+func (*PublishV1Response) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *PublishV1Response) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type ListRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Author int64 `protobuf:"varint,1,opt,name=author,proto3" json:"author,omitempty"`
+ Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
+ Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
+}
+
+func (x *ListRequest) Reset() {
+ *x = ListRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListRequest) ProtoMessage() {}
+
+func (x *ListRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead.
+func (*ListRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *ListRequest) GetAuthor() int64 {
+ if x != nil {
+ return x.Author
+ }
+ return 0
+}
+
+func (x *ListRequest) GetOffset() int32 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+func (x *ListRequest) GetLimit() int32 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type ListResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Articles []*Article `protobuf:"bytes,1,rep,name=articles,proto3" json:"articles,omitempty"`
+}
+
+func (x *ListResponse) Reset() {
+ *x = ListResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListResponse) ProtoMessage() {}
+
+func (x *ListResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[11]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListResponse.ProtoReflect.Descriptor instead.
+func (*ListResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *ListResponse) GetArticles() []*Article {
+ if x != nil {
+ return x.Articles
+ }
+ return nil
+}
+
+type GetByIdRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *GetByIdRequest) Reset() {
+ *x = GetByIdRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdRequest) ProtoMessage() {}
+
+func (x *GetByIdRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[12]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdRequest.ProtoReflect.Descriptor instead.
+func (*GetByIdRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *GetByIdRequest) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type GetByIdResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *GetByIdResponse) Reset() {
+ *x = GetByIdResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdResponse) ProtoMessage() {}
+
+func (x *GetByIdResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[13]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdResponse.ProtoReflect.Descriptor instead.
+func (*GetByIdResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *GetByIdResponse) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type GetPublishedByIdRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Uid int64 `protobuf:"varint,2,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *GetPublishedByIdRequest) Reset() {
+ *x = GetPublishedByIdRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetPublishedByIdRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPublishedByIdRequest) ProtoMessage() {}
+
+func (x *GetPublishedByIdRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[14]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPublishedByIdRequest.ProtoReflect.Descriptor instead.
+func (*GetPublishedByIdRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *GetPublishedByIdRequest) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *GetPublishedByIdRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type GetPublishedByIdResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *GetPublishedByIdResponse) Reset() {
+ *x = GetPublishedByIdResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetPublishedByIdResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPublishedByIdResponse) ProtoMessage() {}
+
+func (x *GetPublishedByIdResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[15]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPublishedByIdResponse.ProtoReflect.Descriptor instead.
+func (*GetPublishedByIdResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *GetPublishedByIdResponse) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type ListPubRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ StartTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"`
+ Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
+ Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
+}
+
+func (x *ListPubRequest) Reset() {
+ *x = ListPubRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListPubRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListPubRequest) ProtoMessage() {}
+
+func (x *ListPubRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[16]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListPubRequest.ProtoReflect.Descriptor instead.
+func (*ListPubRequest) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *ListPubRequest) GetStartTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartTime
+ }
+ return nil
+}
+
+func (x *ListPubRequest) GetOffset() int32 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+func (x *ListPubRequest) GetLimit() int32 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type ListPubResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Articles []*Article `protobuf:"bytes,1,rep,name=articles,proto3" json:"articles,omitempty"`
+}
+
+func (x *ListPubResponse) Reset() {
+ *x = ListPubResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_article_v1_article_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListPubResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListPubResponse) ProtoMessage() {}
+
+func (x *ListPubResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_article_v1_article_proto_msgTypes[17]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListPubResponse.ProtoReflect.Descriptor instead.
+func (*ListPubResponse) Descriptor() ([]byte, []int) {
+ return file_article_v1_article_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *ListPubResponse) GetArticles() []*Article {
+ if x != nil {
+ return x.Articles
+ }
+ return nil
+}
+
+var File_article_v1_article_proto protoreflect.FileDescriptor
+
+var file_article_v1_article_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x61, 0x72, 0x74, 0x69,
+ 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x06, 0x41, 0x75, 0x74, 0x68, 0x6f,
+ 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x8d, 0x02, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+ 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x75, 0x74,
+ 0x68, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x72, 0x74, 0x69,
+ 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x52, 0x06, 0x61,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x30, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x74, 0x69, 0x6d, 0x65,
+ 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+ 0x6d, 0x70, 0x52, 0x05, 0x75, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x62, 0x73,
+ 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x62, 0x73,
+ 0x74, 0x72, 0x61, 0x63, 0x74, 0x22, 0x3c, 0x0a, 0x0b, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e,
+ 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x07, 0x61, 0x72, 0x74, 0x69,
+ 0x63, 0x6c, 0x65, 0x22, 0x1e, 0x0a, 0x0c, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x02, 0x69, 0x64, 0x22, 0x3f, 0x0a, 0x0e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x07, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x22, 0x21, 0x0a, 0x0f, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x33, 0x0a, 0x0f, 0x57, 0x69, 0x74, 0x68, 0x64,
+ 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69,
+ 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x0e, 0x0a, 0x02,
+ 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x12, 0x0a, 0x10,
+ 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x22, 0x41, 0x0a, 0x10, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x56, 0x31, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e,
+ 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x07, 0x61, 0x72, 0x74, 0x69,
+ 0x63, 0x6c, 0x65, 0x22, 0x23, 0x0a, 0x11, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x56, 0x31,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x53, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f,
+ 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12,
+ 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
+ 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x3f, 0x0a,
+ 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a,
+ 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x13, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73, 0x22, 0x20,
+ 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64,
+ 0x22, 0x40, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76,
+ 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63,
+ 0x6c, 0x65, 0x22, 0x3b, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68,
+ 0x65, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a,
+ 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a,
+ 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22,
+ 0x49, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42,
+ 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x07, 0x61,
+ 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61,
+ 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x52, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x22, 0x79, 0x0a, 0x0e, 0x4c, 0x69,
+ 0x73, 0x74, 0x50, 0x75, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x0a,
+ 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74,
+ 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65,
+ 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12,
+ 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
+ 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x42, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69,
+ 0x63, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52,
+ 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73, 0x32, 0xf8, 0x03, 0x0a, 0x0e, 0x41, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x04,
+ 0x53, 0x61, 0x76, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76,
+ 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e,
+ 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x50, 0x75, 0x62, 0x6c, 0x69,
+ 0x73, 0x68, 0x12, 0x1a, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
+ 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
+ 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c,
+ 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x08, 0x57,
+ 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76,
+ 0x31, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x17, 0x2e, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31,
+ 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a,
+ 0x07, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x12, 0x1a, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63,
+ 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76,
+ 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x5d, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65,
+ 0x64, 0x42, 0x79, 0x49, 0x64, 0x12, 0x23, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42,
+ 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69,
+ 0x73, 0x68, 0x65, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x42, 0x0a, 0x07, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x12, 0x1a, 0x2e, 0x61, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xae, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63,
+ 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69,
+ 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x76, 0x31, 0xa2, 0x02,
+ 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x56,
+ 0x31, 0xca, 0x02, 0x0a, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02,
+ 0x16, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d,
+ 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0b, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_article_v1_article_proto_rawDescOnce sync.Once
+ file_article_v1_article_proto_rawDescData = file_article_v1_article_proto_rawDesc
+)
+
+func file_article_v1_article_proto_rawDescGZIP() []byte {
+ file_article_v1_article_proto_rawDescOnce.Do(func() {
+ file_article_v1_article_proto_rawDescData = protoimpl.X.CompressGZIP(file_article_v1_article_proto_rawDescData)
+ })
+ return file_article_v1_article_proto_rawDescData
+}
+
+var file_article_v1_article_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
+var file_article_v1_article_proto_goTypes = []interface{}{
+ (*Author)(nil), // 0: article.v1.Author
+ (*Article)(nil), // 1: article.v1.Article
+ (*SaveRequest)(nil), // 2: article.v1.SaveRequest
+ (*SaveResponse)(nil), // 3: article.v1.SaveResponse
+ (*PublishRequest)(nil), // 4: article.v1.PublishRequest
+ (*PublishResponse)(nil), // 5: article.v1.PublishResponse
+ (*WithdrawRequest)(nil), // 6: article.v1.WithdrawRequest
+ (*WithdrawResponse)(nil), // 7: article.v1.WithdrawResponse
+ (*PublishV1Request)(nil), // 8: article.v1.PublishV1Request
+ (*PublishV1Response)(nil), // 9: article.v1.PublishV1Response
+ (*ListRequest)(nil), // 10: article.v1.ListRequest
+ (*ListResponse)(nil), // 11: article.v1.ListResponse
+ (*GetByIdRequest)(nil), // 12: article.v1.GetByIdRequest
+ (*GetByIdResponse)(nil), // 13: article.v1.GetByIdResponse
+ (*GetPublishedByIdRequest)(nil), // 14: article.v1.GetPublishedByIdRequest
+ (*GetPublishedByIdResponse)(nil), // 15: article.v1.GetPublishedByIdResponse
+ (*ListPubRequest)(nil), // 16: article.v1.ListPubRequest
+ (*ListPubResponse)(nil), // 17: article.v1.ListPubResponse
+ (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
+}
+var file_article_v1_article_proto_depIdxs = []int32{
+ 0, // 0: article.v1.Article.author:type_name -> article.v1.Author
+ 18, // 1: article.v1.Article.ctime:type_name -> google.protobuf.Timestamp
+ 18, // 2: article.v1.Article.utime:type_name -> google.protobuf.Timestamp
+ 1, // 3: article.v1.SaveRequest.article:type_name -> article.v1.Article
+ 1, // 4: article.v1.PublishRequest.article:type_name -> article.v1.Article
+ 1, // 5: article.v1.PublishV1Request.article:type_name -> article.v1.Article
+ 1, // 6: article.v1.ListResponse.articles:type_name -> article.v1.Article
+ 1, // 7: article.v1.GetByIdResponse.article:type_name -> article.v1.Article
+ 1, // 8: article.v1.GetPublishedByIdResponse.article:type_name -> article.v1.Article
+ 18, // 9: article.v1.ListPubRequest.start_time:type_name -> google.protobuf.Timestamp
+ 1, // 10: article.v1.ListPubResponse.articles:type_name -> article.v1.Article
+ 2, // 11: article.v1.ArticleService.Save:input_type -> article.v1.SaveRequest
+ 4, // 12: article.v1.ArticleService.Publish:input_type -> article.v1.PublishRequest
+ 6, // 13: article.v1.ArticleService.Withdraw:input_type -> article.v1.WithdrawRequest
+ 10, // 14: article.v1.ArticleService.List:input_type -> article.v1.ListRequest
+ 12, // 15: article.v1.ArticleService.GetById:input_type -> article.v1.GetByIdRequest
+ 14, // 16: article.v1.ArticleService.GetPublishedById:input_type -> article.v1.GetPublishedByIdRequest
+ 16, // 17: article.v1.ArticleService.ListPub:input_type -> article.v1.ListPubRequest
+ 3, // 18: article.v1.ArticleService.Save:output_type -> article.v1.SaveResponse
+ 5, // 19: article.v1.ArticleService.Publish:output_type -> article.v1.PublishResponse
+ 7, // 20: article.v1.ArticleService.Withdraw:output_type -> article.v1.WithdrawResponse
+ 11, // 21: article.v1.ArticleService.List:output_type -> article.v1.ListResponse
+ 13, // 22: article.v1.ArticleService.GetById:output_type -> article.v1.GetByIdResponse
+ 15, // 23: article.v1.ArticleService.GetPublishedById:output_type -> article.v1.GetPublishedByIdResponse
+ 17, // 24: article.v1.ArticleService.ListPub:output_type -> article.v1.ListPubResponse
+ 18, // [18:25] is the sub-list for method output_type
+ 11, // [11:18] is the sub-list for method input_type
+ 11, // [11:11] is the sub-list for extension type_name
+ 11, // [11:11] is the sub-list for extension extendee
+ 0, // [0:11] is the sub-list for field type_name
+}
+
+func init() { file_article_v1_article_proto_init() }
+func file_article_v1_article_proto_init() {
+ if File_article_v1_article_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_article_v1_article_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Author); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Article); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SaveRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SaveResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PublishRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PublishResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*WithdrawRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*WithdrawResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PublishV1Request); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PublishV1Response); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetPublishedByIdRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetPublishedByIdResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListPubRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_article_v1_article_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListPubResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_article_v1_article_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 18,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_article_v1_article_proto_goTypes,
+ DependencyIndexes: file_article_v1_article_proto_depIdxs,
+ MessageInfos: file_article_v1_article_proto_msgTypes,
+ }.Build()
+ File_article_v1_article_proto = out.File
+ file_article_v1_article_proto_rawDesc = nil
+ file_article_v1_article_proto_goTypes = nil
+ file_article_v1_article_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/article/v1/article_grpc.pb.go b/webook/api/proto/gen/article/v1/article_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca7fffd87033bb1eed46e39ca0d6ea5947f67808
--- /dev/null
+++ b/webook/api/proto/gen/article/v1/article_grpc.pb.go
@@ -0,0 +1,331 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: article/v1/article.proto
+
+package articlev1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ ArticleService_Save_FullMethodName = "/article.v1.ArticleService/Save"
+ ArticleService_Publish_FullMethodName = "/article.v1.ArticleService/Publish"
+ ArticleService_Withdraw_FullMethodName = "/article.v1.ArticleService/Withdraw"
+ ArticleService_List_FullMethodName = "/article.v1.ArticleService/List"
+ ArticleService_GetById_FullMethodName = "/article.v1.ArticleService/GetById"
+ ArticleService_GetPublishedById_FullMethodName = "/article.v1.ArticleService/GetPublishedById"
+ ArticleService_ListPub_FullMethodName = "/article.v1.ArticleService/ListPub"
+)
+
+// ArticleServiceClient is the client API for ArticleService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ArticleServiceClient interface {
+ Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*SaveResponse, error)
+ Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error)
+ Withdraw(ctx context.Context, in *WithdrawRequest, opts ...grpc.CallOption) (*WithdrawResponse, error)
+ List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
+ GetById(ctx context.Context, in *GetByIdRequest, opts ...grpc.CallOption) (*GetByIdResponse, error)
+ GetPublishedById(ctx context.Context, in *GetPublishedByIdRequest, opts ...grpc.CallOption) (*GetPublishedByIdResponse, error)
+ ListPub(ctx context.Context, in *ListPubRequest, opts ...grpc.CallOption) (*ListPubResponse, error)
+}
+
+type articleServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewArticleServiceClient(cc grpc.ClientConnInterface) ArticleServiceClient {
+ return &articleServiceClient{cc}
+}
+
+func (c *articleServiceClient) Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*SaveResponse, error) {
+ out := new(SaveResponse)
+ err := c.cc.Invoke(ctx, ArticleService_Save_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) Publish(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) {
+ out := new(PublishResponse)
+ err := c.cc.Invoke(ctx, ArticleService_Publish_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) Withdraw(ctx context.Context, in *WithdrawRequest, opts ...grpc.CallOption) (*WithdrawResponse, error) {
+ out := new(WithdrawResponse)
+ err := c.cc.Invoke(ctx, ArticleService_Withdraw_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
+ out := new(ListResponse)
+ err := c.cc.Invoke(ctx, ArticleService_List_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) GetById(ctx context.Context, in *GetByIdRequest, opts ...grpc.CallOption) (*GetByIdResponse, error) {
+ out := new(GetByIdResponse)
+ err := c.cc.Invoke(ctx, ArticleService_GetById_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) GetPublishedById(ctx context.Context, in *GetPublishedByIdRequest, opts ...grpc.CallOption) (*GetPublishedByIdResponse, error) {
+ out := new(GetPublishedByIdResponse)
+ err := c.cc.Invoke(ctx, ArticleService_GetPublishedById_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *articleServiceClient) ListPub(ctx context.Context, in *ListPubRequest, opts ...grpc.CallOption) (*ListPubResponse, error) {
+ out := new(ListPubResponse)
+ err := c.cc.Invoke(ctx, ArticleService_ListPub_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// ArticleServiceServer is the server API for ArticleService service.
+// All implementations must embed UnimplementedArticleServiceServer
+// for forward compatibility
+type ArticleServiceServer interface {
+ Save(context.Context, *SaveRequest) (*SaveResponse, error)
+ Publish(context.Context, *PublishRequest) (*PublishResponse, error)
+ Withdraw(context.Context, *WithdrawRequest) (*WithdrawResponse, error)
+ List(context.Context, *ListRequest) (*ListResponse, error)
+ GetById(context.Context, *GetByIdRequest) (*GetByIdResponse, error)
+ GetPublishedById(context.Context, *GetPublishedByIdRequest) (*GetPublishedByIdResponse, error)
+ ListPub(context.Context, *ListPubRequest) (*ListPubResponse, error)
+ mustEmbedUnimplementedArticleServiceServer()
+}
+
+// UnimplementedArticleServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedArticleServiceServer struct {
+}
+
+func (UnimplementedArticleServiceServer) Save(context.Context, *SaveRequest) (*SaveResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Save not implemented")
+}
+func (UnimplementedArticleServiceServer) Publish(context.Context, *PublishRequest) (*PublishResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented")
+}
+func (UnimplementedArticleServiceServer) Withdraw(context.Context, *WithdrawRequest) (*WithdrawResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Withdraw not implemented")
+}
+func (UnimplementedArticleServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
+}
+func (UnimplementedArticleServiceServer) GetById(context.Context, *GetByIdRequest) (*GetByIdResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetById not implemented")
+}
+func (UnimplementedArticleServiceServer) GetPublishedById(context.Context, *GetPublishedByIdRequest) (*GetPublishedByIdResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetPublishedById not implemented")
+}
+func (UnimplementedArticleServiceServer) ListPub(context.Context, *ListPubRequest) (*ListPubResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ListPub not implemented")
+}
+func (UnimplementedArticleServiceServer) mustEmbedUnimplementedArticleServiceServer() {}
+
+// UnsafeArticleServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ArticleServiceServer will
+// result in compilation errors.
+type UnsafeArticleServiceServer interface {
+ mustEmbedUnimplementedArticleServiceServer()
+}
+
+func RegisterArticleServiceServer(s grpc.ServiceRegistrar, srv ArticleServiceServer) {
+ s.RegisterService(&ArticleService_ServiceDesc, srv)
+}
+
+func _ArticleService_Save_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SaveRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).Save(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_Save_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).Save(ctx, req.(*SaveRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(PublishRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).Publish(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_Publish_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).Publish(ctx, req.(*PublishRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_Withdraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(WithdrawRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).Withdraw(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_Withdraw_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).Withdraw(ctx, req.(*WithdrawRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).List(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_List_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).List(ctx, req.(*ListRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_GetById_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetByIdRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).GetById(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_GetById_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).GetById(ctx, req.(*GetByIdRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_GetPublishedById_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetPublishedByIdRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).GetPublishedById(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_GetPublishedById_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).GetPublishedById(ctx, req.(*GetPublishedByIdRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ArticleService_ListPub_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListPubRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ArticleServiceServer).ListPub(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ArticleService_ListPub_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ArticleServiceServer).ListPub(ctx, req.(*ListPubRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// ArticleService_ServiceDesc is the grpc.ServiceDesc for ArticleService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ArticleService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "article.v1.ArticleService",
+ HandlerType: (*ArticleServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Save",
+ Handler: _ArticleService_Save_Handler,
+ },
+ {
+ MethodName: "Publish",
+ Handler: _ArticleService_Publish_Handler,
+ },
+ {
+ MethodName: "Withdraw",
+ Handler: _ArticleService_Withdraw_Handler,
+ },
+ {
+ MethodName: "List",
+ Handler: _ArticleService_List_Handler,
+ },
+ {
+ MethodName: "GetById",
+ Handler: _ArticleService_GetById_Handler,
+ },
+ {
+ MethodName: "GetPublishedById",
+ Handler: _ArticleService_GetPublishedById_Handler,
+ },
+ {
+ MethodName: "ListPub",
+ Handler: _ArticleService_ListPub_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "article/v1/article.proto",
+}
diff --git a/webook/api/proto/gen/article/v1/mocks/article_grpc.mock.go b/webook/api/proto/gen/article/v1/mocks/article_grpc.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..11086c5f9c55611c591a1c64f6a37500d9379a91
--- /dev/null
+++ b/webook/api/proto/gen/article/v1/mocks/article_grpc.mock.go
@@ -0,0 +1,356 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/api/proto/gen/article/v1/article_grpc.pb.go
+//
+// Generated by this command:
+//
+// mockgen -source=webook/api/proto/gen/article/v1/article_grpc.pb.go -package=artmocks -destination=webook/api/proto/gen/article/v1/mocks/article_grpc.mock.go
+//
+// Package artmocks is a generated GoMock package.
+package artmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ articlev1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/article/v1"
+ gomock "go.uber.org/mock/gomock"
+ grpc "google.golang.org/grpc"
+)
+
+// MockArticleServiceClient is a mock of ArticleServiceClient interface.
+type MockArticleServiceClient struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleServiceClientMockRecorder
+}
+
+// MockArticleServiceClientMockRecorder is the mock recorder for MockArticleServiceClient.
+type MockArticleServiceClientMockRecorder struct {
+ mock *MockArticleServiceClient
+}
+
+// NewMockArticleServiceClient creates a new mock instance.
+func NewMockArticleServiceClient(ctrl *gomock.Controller) *MockArticleServiceClient {
+ mock := &MockArticleServiceClient{ctrl: ctrl}
+ mock.recorder = &MockArticleServiceClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleServiceClient) EXPECT() *MockArticleServiceClientMockRecorder {
+ return m.recorder
+}
+
+// GetById mocks base method.
+func (m *MockArticleServiceClient) GetById(ctx context.Context, in *articlev1.GetByIdRequest, opts ...grpc.CallOption) (*articlev1.GetByIdResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetById", varargs...)
+ ret0, _ := ret[0].(*articlev1.GetByIdResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetById indicates an expected call of GetById.
+func (mr *MockArticleServiceClientMockRecorder) GetById(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetById", reflect.TypeOf((*MockArticleServiceClient)(nil).GetById), varargs...)
+}
+
+// GetPublishedById mocks base method.
+func (m *MockArticleServiceClient) GetPublishedById(ctx context.Context, in *articlev1.GetPublishedByIdRequest, opts ...grpc.CallOption) (*articlev1.GetPublishedByIdResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetPublishedById", varargs...)
+ ret0, _ := ret[0].(*articlev1.GetPublishedByIdResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPublishedById indicates an expected call of GetPublishedById.
+func (mr *MockArticleServiceClientMockRecorder) GetPublishedById(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishedById", reflect.TypeOf((*MockArticleServiceClient)(nil).GetPublishedById), varargs...)
+}
+
+// List mocks base method.
+func (m *MockArticleServiceClient) List(ctx context.Context, in *articlev1.ListRequest, opts ...grpc.CallOption) (*articlev1.ListResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "List", varargs...)
+ ret0, _ := ret[0].(*articlev1.ListResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockArticleServiceClientMockRecorder) List(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockArticleServiceClient)(nil).List), varargs...)
+}
+
+// ListPub mocks base method.
+func (m *MockArticleServiceClient) ListPub(ctx context.Context, in *articlev1.ListPubRequest, opts ...grpc.CallOption) (*articlev1.ListPubResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ListPub", varargs...)
+ ret0, _ := ret[0].(*articlev1.ListPubResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPub indicates an expected call of ListPub.
+func (mr *MockArticleServiceClientMockRecorder) ListPub(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPub", reflect.TypeOf((*MockArticleServiceClient)(nil).ListPub), varargs...)
+}
+
+// Publish mocks base method.
+func (m *MockArticleServiceClient) Publish(ctx context.Context, in *articlev1.PublishRequest, opts ...grpc.CallOption) (*articlev1.PublishResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Publish", varargs...)
+ ret0, _ := ret[0].(*articlev1.PublishResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Publish indicates an expected call of Publish.
+func (mr *MockArticleServiceClientMockRecorder) Publish(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockArticleServiceClient)(nil).Publish), varargs...)
+}
+
+// Save mocks base method.
+func (m *MockArticleServiceClient) Save(ctx context.Context, in *articlev1.SaveRequest, opts ...grpc.CallOption) (*articlev1.SaveResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Save", varargs...)
+ ret0, _ := ret[0].(*articlev1.SaveResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Save indicates an expected call of Save.
+func (mr *MockArticleServiceClientMockRecorder) Save(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockArticleServiceClient)(nil).Save), varargs...)
+}
+
+// Withdraw mocks base method.
+func (m *MockArticleServiceClient) Withdraw(ctx context.Context, in *articlev1.WithdrawRequest, opts ...grpc.CallOption) (*articlev1.WithdrawResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Withdraw", varargs...)
+ ret0, _ := ret[0].(*articlev1.WithdrawResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Withdraw indicates an expected call of Withdraw.
+func (mr *MockArticleServiceClientMockRecorder) Withdraw(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockArticleServiceClient)(nil).Withdraw), varargs...)
+}
+
+// MockArticleServiceServer is a mock of ArticleServiceServer interface.
+type MockArticleServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleServiceServerMockRecorder
+}
+
+// MockArticleServiceServerMockRecorder is the mock recorder for MockArticleServiceServer.
+type MockArticleServiceServerMockRecorder struct {
+ mock *MockArticleServiceServer
+}
+
+// NewMockArticleServiceServer creates a new mock instance.
+func NewMockArticleServiceServer(ctrl *gomock.Controller) *MockArticleServiceServer {
+ mock := &MockArticleServiceServer{ctrl: ctrl}
+ mock.recorder = &MockArticleServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleServiceServer) EXPECT() *MockArticleServiceServerMockRecorder {
+ return m.recorder
+}
+
+// GetById mocks base method.
+func (m *MockArticleServiceServer) GetById(arg0 context.Context, arg1 *articlev1.GetByIdRequest) (*articlev1.GetByIdResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetById", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.GetByIdResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetById indicates an expected call of GetById.
+func (mr *MockArticleServiceServerMockRecorder) GetById(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetById", reflect.TypeOf((*MockArticleServiceServer)(nil).GetById), arg0, arg1)
+}
+
+// GetPublishedById mocks base method.
+func (m *MockArticleServiceServer) GetPublishedById(arg0 context.Context, arg1 *articlev1.GetPublishedByIdRequest) (*articlev1.GetPublishedByIdResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPublishedById", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.GetPublishedByIdResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPublishedById indicates an expected call of GetPublishedById.
+func (mr *MockArticleServiceServerMockRecorder) GetPublishedById(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishedById", reflect.TypeOf((*MockArticleServiceServer)(nil).GetPublishedById), arg0, arg1)
+}
+
+// List mocks base method.
+func (m *MockArticleServiceServer) List(arg0 context.Context, arg1 *articlev1.ListRequest) (*articlev1.ListResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "List", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.ListResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockArticleServiceServerMockRecorder) List(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockArticleServiceServer)(nil).List), arg0, arg1)
+}
+
+// ListPub mocks base method.
+func (m *MockArticleServiceServer) ListPub(arg0 context.Context, arg1 *articlev1.ListPubRequest) (*articlev1.ListPubResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPub", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.ListPubResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPub indicates an expected call of ListPub.
+func (mr *MockArticleServiceServerMockRecorder) ListPub(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPub", reflect.TypeOf((*MockArticleServiceServer)(nil).ListPub), arg0, arg1)
+}
+
+// Publish mocks base method.
+func (m *MockArticleServiceServer) Publish(arg0 context.Context, arg1 *articlev1.PublishRequest) (*articlev1.PublishResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Publish", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.PublishResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Publish indicates an expected call of Publish.
+func (mr *MockArticleServiceServerMockRecorder) Publish(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockArticleServiceServer)(nil).Publish), arg0, arg1)
+}
+
+// Save mocks base method.
+func (m *MockArticleServiceServer) Save(arg0 context.Context, arg1 *articlev1.SaveRequest) (*articlev1.SaveResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.SaveResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Save indicates an expected call of Save.
+func (mr *MockArticleServiceServerMockRecorder) Save(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockArticleServiceServer)(nil).Save), arg0, arg1)
+}
+
+// Withdraw mocks base method.
+func (m *MockArticleServiceServer) Withdraw(arg0 context.Context, arg1 *articlev1.WithdrawRequest) (*articlev1.WithdrawResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Withdraw", arg0, arg1)
+ ret0, _ := ret[0].(*articlev1.WithdrawResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Withdraw indicates an expected call of Withdraw.
+func (mr *MockArticleServiceServerMockRecorder) Withdraw(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockArticleServiceServer)(nil).Withdraw), arg0, arg1)
+}
+
+// mustEmbedUnimplementedArticleServiceServer mocks base method.
+func (m *MockArticleServiceServer) mustEmbedUnimplementedArticleServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedArticleServiceServer")
+}
+
+// mustEmbedUnimplementedArticleServiceServer indicates an expected call of mustEmbedUnimplementedArticleServiceServer.
+func (mr *MockArticleServiceServerMockRecorder) mustEmbedUnimplementedArticleServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedArticleServiceServer", reflect.TypeOf((*MockArticleServiceServer)(nil).mustEmbedUnimplementedArticleServiceServer))
+}
+
+// MockUnsafeArticleServiceServer is a mock of UnsafeArticleServiceServer interface.
+type MockUnsafeArticleServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockUnsafeArticleServiceServerMockRecorder
+}
+
+// MockUnsafeArticleServiceServerMockRecorder is the mock recorder for MockUnsafeArticleServiceServer.
+type MockUnsafeArticleServiceServerMockRecorder struct {
+ mock *MockUnsafeArticleServiceServer
+}
+
+// NewMockUnsafeArticleServiceServer creates a new mock instance.
+func NewMockUnsafeArticleServiceServer(ctrl *gomock.Controller) *MockUnsafeArticleServiceServer {
+ mock := &MockUnsafeArticleServiceServer{ctrl: ctrl}
+ mock.recorder = &MockUnsafeArticleServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUnsafeArticleServiceServer) EXPECT() *MockUnsafeArticleServiceServerMockRecorder {
+ return m.recorder
+}
+
+// mustEmbedUnimplementedArticleServiceServer mocks base method.
+func (m *MockUnsafeArticleServiceServer) mustEmbedUnimplementedArticleServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedArticleServiceServer")
+}
+
+// mustEmbedUnimplementedArticleServiceServer indicates an expected call of mustEmbedUnimplementedArticleServiceServer.
+func (mr *MockUnsafeArticleServiceServerMockRecorder) mustEmbedUnimplementedArticleServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedArticleServiceServer", reflect.TypeOf((*MockUnsafeArticleServiceServer)(nil).mustEmbedUnimplementedArticleServiceServer))
+}
diff --git a/webook/api/proto/gen/code/v1/code.pb.go b/webook/api/proto/gen/code/v1/code.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..f10f9f325047bcc1290ec4284c7d63c080345e03
--- /dev/null
+++ b/webook/api/proto/gen/code/v1/code.pb.go
@@ -0,0 +1,369 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: code/v1/code.proto
+
+package codev1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CodeSendRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ Phone string `protobuf:"bytes,2,opt,name=phone,proto3" json:"phone,omitempty"`
+}
+
+func (x *CodeSendRequest) Reset() {
+ *x = CodeSendRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_code_v1_code_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CodeSendRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CodeSendRequest) ProtoMessage() {}
+
+func (x *CodeSendRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_code_v1_code_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CodeSendRequest.ProtoReflect.Descriptor instead.
+func (*CodeSendRequest) Descriptor() ([]byte, []int) {
+ return file_code_v1_code_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CodeSendRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CodeSendRequest) GetPhone() string {
+ if x != nil {
+ return x.Phone
+ }
+ return ""
+}
+
+type CodeSendResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CodeSendResponse) Reset() {
+ *x = CodeSendResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_code_v1_code_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CodeSendResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CodeSendResponse) ProtoMessage() {}
+
+func (x *CodeSendResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_code_v1_code_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CodeSendResponse.ProtoReflect.Descriptor instead.
+func (*CodeSendResponse) Descriptor() ([]byte, []int) {
+ return file_code_v1_code_proto_rawDescGZIP(), []int{1}
+}
+
+type VerifyRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ Phone string `protobuf:"bytes,2,opt,name=phone,proto3" json:"phone,omitempty"`
+ InputCode string `protobuf:"bytes,3,opt,name=inputCode,proto3" json:"inputCode,omitempty"`
+}
+
+func (x *VerifyRequest) Reset() {
+ *x = VerifyRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_code_v1_code_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *VerifyRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyRequest) ProtoMessage() {}
+
+func (x *VerifyRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_code_v1_code_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyRequest.ProtoReflect.Descriptor instead.
+func (*VerifyRequest) Descriptor() ([]byte, []int) {
+ return file_code_v1_code_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *VerifyRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *VerifyRequest) GetPhone() string {
+ if x != nil {
+ return x.Phone
+ }
+ return ""
+}
+
+func (x *VerifyRequest) GetInputCode() string {
+ if x != nil {
+ return x.InputCode
+ }
+ return ""
+}
+
+type VerifyResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Answer bool `protobuf:"varint,1,opt,name=answer,proto3" json:"answer,omitempty"`
+}
+
+func (x *VerifyResponse) Reset() {
+ *x = VerifyResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_code_v1_code_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *VerifyResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyResponse) ProtoMessage() {}
+
+func (x *VerifyResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_code_v1_code_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyResponse.ProtoReflect.Descriptor instead.
+func (*VerifyResponse) Descriptor() ([]byte, []int) {
+ return file_code_v1_code_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *VerifyResponse) GetAnswer() bool {
+ if x != nil {
+ return x.Answer
+ }
+ return false
+}
+
+var File_code_v1_code_proto protoreflect.FileDescriptor
+
+var file_code_v1_code_proto_rawDesc = []byte{
+ 0x0a, 0x12, 0x63, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x39, 0x0a,
+ 0x0f, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62,
+ 0x69, 0x7a, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x43, 0x6f, 0x64, 0x65,
+ 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x55, 0x0a, 0x0d,
+ 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
+ 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12,
+ 0x14, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
+ 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x43, 0x6f,
+ 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x43,
+ 0x6f, 0x64, 0x65, 0x22, 0x28, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x32, 0x85, 0x01,
+ 0x0a, 0x0b, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3b, 0x0a,
+ 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e,
+ 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65,
+ 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x56, 0x65,
+ 0x72, 0x69, 0x66, 0x79, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56,
+ 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63,
+ 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x96, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f,
+ 0x64, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x43, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
+ 0x50, 0x01, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65,
+ 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f,
+ 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x64,
+ 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x43, 0x6f, 0x64, 0x65,
+ 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x43, 0x6f, 0x64, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13,
+ 0x43, 0x6f, 0x64, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
+ 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x43, 0x6f, 0x64, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_code_v1_code_proto_rawDescOnce sync.Once
+ file_code_v1_code_proto_rawDescData = file_code_v1_code_proto_rawDesc
+)
+
+func file_code_v1_code_proto_rawDescGZIP() []byte {
+ file_code_v1_code_proto_rawDescOnce.Do(func() {
+ file_code_v1_code_proto_rawDescData = protoimpl.X.CompressGZIP(file_code_v1_code_proto_rawDescData)
+ })
+ return file_code_v1_code_proto_rawDescData
+}
+
+var file_code_v1_code_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_code_v1_code_proto_goTypes = []interface{}{
+ (*CodeSendRequest)(nil), // 0: code.v1.CodeSendRequest
+ (*CodeSendResponse)(nil), // 1: code.v1.CodeSendResponse
+ (*VerifyRequest)(nil), // 2: code.v1.VerifyRequest
+ (*VerifyResponse)(nil), // 3: code.v1.VerifyResponse
+}
+var file_code_v1_code_proto_depIdxs = []int32{
+ 0, // 0: code.v1.CodeService.Send:input_type -> code.v1.CodeSendRequest
+ 2, // 1: code.v1.CodeService.Verify:input_type -> code.v1.VerifyRequest
+ 1, // 2: code.v1.CodeService.Send:output_type -> code.v1.CodeSendResponse
+ 3, // 3: code.v1.CodeService.Verify:output_type -> code.v1.VerifyResponse
+ 2, // [2:4] is the sub-list for method output_type
+ 0, // [0:2] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_code_v1_code_proto_init() }
+func file_code_v1_code_proto_init() {
+ if File_code_v1_code_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_code_v1_code_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CodeSendRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_code_v1_code_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CodeSendResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_code_v1_code_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*VerifyRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_code_v1_code_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*VerifyResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_code_v1_code_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_code_v1_code_proto_goTypes,
+ DependencyIndexes: file_code_v1_code_proto_depIdxs,
+ MessageInfos: file_code_v1_code_proto_msgTypes,
+ }.Build()
+ File_code_v1_code_proto = out.File
+ file_code_v1_code_proto_rawDesc = nil
+ file_code_v1_code_proto_goTypes = nil
+ file_code_v1_code_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/code/v1/code_grpc.pb.go b/webook/api/proto/gen/code/v1/code_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7c957307d7a123a4810532b6c3dedee1c71a6a8
--- /dev/null
+++ b/webook/api/proto/gen/code/v1/code_grpc.pb.go
@@ -0,0 +1,146 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: code/v1/code.proto
+
+package codev1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ CodeService_Send_FullMethodName = "/code.v1.CodeService/Send"
+ CodeService_Verify_FullMethodName = "/code.v1.CodeService/Verify"
+)
+
+// CodeServiceClient is the client API for CodeService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CodeServiceClient interface {
+ Send(ctx context.Context, in *CodeSendRequest, opts ...grpc.CallOption) (*CodeSendResponse, error)
+ Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyResponse, error)
+}
+
+type codeServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewCodeServiceClient(cc grpc.ClientConnInterface) CodeServiceClient {
+ return &codeServiceClient{cc}
+}
+
+func (c *codeServiceClient) Send(ctx context.Context, in *CodeSendRequest, opts ...grpc.CallOption) (*CodeSendResponse, error) {
+ out := new(CodeSendResponse)
+ err := c.cc.Invoke(ctx, CodeService_Send_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *codeServiceClient) Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyResponse, error) {
+ out := new(VerifyResponse)
+ err := c.cc.Invoke(ctx, CodeService_Verify_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// CodeServiceServer is the server API for CodeService service.
+// All implementations must embed UnimplementedCodeServiceServer
+// for forward compatibility
+type CodeServiceServer interface {
+ Send(context.Context, *CodeSendRequest) (*CodeSendResponse, error)
+ Verify(context.Context, *VerifyRequest) (*VerifyResponse, error)
+ mustEmbedUnimplementedCodeServiceServer()
+}
+
+// UnimplementedCodeServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedCodeServiceServer struct {
+}
+
+func (UnimplementedCodeServiceServer) Send(context.Context, *CodeSendRequest) (*CodeSendResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
+}
+func (UnimplementedCodeServiceServer) Verify(context.Context, *VerifyRequest) (*VerifyResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Verify not implemented")
+}
+func (UnimplementedCodeServiceServer) mustEmbedUnimplementedCodeServiceServer() {}
+
+// UnsafeCodeServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CodeServiceServer will
+// result in compilation errors.
+type UnsafeCodeServiceServer interface {
+ mustEmbedUnimplementedCodeServiceServer()
+}
+
+func RegisterCodeServiceServer(s grpc.ServiceRegistrar, srv CodeServiceServer) {
+ s.RegisterService(&CodeService_ServiceDesc, srv)
+}
+
+func _CodeService_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CodeSendRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CodeServiceServer).Send(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CodeService_Send_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CodeServiceServer).Send(ctx, req.(*CodeSendRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CodeService_Verify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(VerifyRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CodeServiceServer).Verify(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CodeService_Verify_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CodeServiceServer).Verify(ctx, req.(*VerifyRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// CodeService_ServiceDesc is the grpc.ServiceDesc for CodeService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CodeService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "code.v1.CodeService",
+ HandlerType: (*CodeServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Send",
+ Handler: _CodeService_Send_Handler,
+ },
+ {
+ MethodName: "Verify",
+ Handler: _CodeService_Verify_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "code/v1/code.proto",
+}
diff --git a/webook/api/proto/gen/comment/v1/comment.pb.go b/webook/api/proto/gen/comment/v1/comment.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a0dd6082814f9022a3c903bb98d19c0aee0590c
--- /dev/null
+++ b/webook/api/proto/gen/comment/v1/comment.pb.go
@@ -0,0 +1,815 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: comment/v1/comment.proto
+
+package commentv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// 安排评论时间排序,在使用自增主键的情况下,实际上就是按照主键大小排序,倒序
+type CommentListRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ Bizid int64 `protobuf:"varint,2,opt,name=bizid,proto3" json:"bizid,omitempty"`
+ MinId int64 `protobuf:"varint,3,opt,name=min_id,json=minId,proto3" json:"min_id,omitempty"`
+ Limit int64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
+}
+
+func (x *CommentListRequest) Reset() {
+ *x = CommentListRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CommentListRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CommentListRequest) ProtoMessage() {}
+
+func (x *CommentListRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CommentListRequest.ProtoReflect.Descriptor instead.
+func (*CommentListRequest) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CommentListRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CommentListRequest) GetBizid() int64 {
+ if x != nil {
+ return x.Bizid
+ }
+ return 0
+}
+
+func (x *CommentListRequest) GetMinId() int64 {
+ if x != nil {
+ return x.MinId
+ }
+ return 0
+}
+
+func (x *CommentListRequest) GetLimit() int64 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type CommentListResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Comments []*Comment `protobuf:"bytes,1,rep,name=comments,proto3" json:"comments,omitempty"`
+}
+
+func (x *CommentListResponse) Reset() {
+ *x = CommentListResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CommentListResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CommentListResponse) ProtoMessage() {}
+
+func (x *CommentListResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CommentListResponse.ProtoReflect.Descriptor instead.
+func (*CommentListResponse) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CommentListResponse) GetComments() []*Comment {
+ if x != nil {
+ return x.Comments
+ }
+ return nil
+}
+
+type DeleteCommentRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *DeleteCommentRequest) Reset() {
+ *x = DeleteCommentRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DeleteCommentRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteCommentRequest) ProtoMessage() {}
+
+func (x *DeleteCommentRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteCommentRequest.ProtoReflect.Descriptor instead.
+func (*DeleteCommentRequest) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *DeleteCommentRequest) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type DeleteCommentResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *DeleteCommentResponse) Reset() {
+ *x = DeleteCommentResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DeleteCommentResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteCommentResponse) ProtoMessage() {}
+
+func (x *DeleteCommentResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteCommentResponse.ProtoReflect.Descriptor instead.
+func (*DeleteCommentResponse) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{3}
+}
+
+type CreateCommentRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Comment *Comment `protobuf:"bytes,1,opt,name=comment,proto3" json:"comment,omitempty"`
+}
+
+func (x *CreateCommentRequest) Reset() {
+ *x = CreateCommentRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateCommentRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateCommentRequest) ProtoMessage() {}
+
+func (x *CreateCommentRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateCommentRequest.ProtoReflect.Descriptor instead.
+func (*CreateCommentRequest) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *CreateCommentRequest) GetComment() *Comment {
+ if x != nil {
+ return x.Comment
+ }
+ return nil
+}
+
+type CreateCommentResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CreateCommentResponse) Reset() {
+ *x = CreateCommentResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateCommentResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateCommentResponse) ProtoMessage() {}
+
+func (x *CreateCommentResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateCommentResponse.ProtoReflect.Descriptor instead.
+func (*CreateCommentResponse) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{5}
+}
+
+type GetMoreRepliesRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Rid int64 `protobuf:"varint,1,opt,name=rid,proto3" json:"rid,omitempty"`
+ MaxId int64 `protobuf:"varint,2,opt,name=max_id,json=maxId,proto3" json:"max_id,omitempty"`
+ Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
+}
+
+func (x *GetMoreRepliesRequest) Reset() {
+ *x = GetMoreRepliesRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetMoreRepliesRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetMoreRepliesRequest) ProtoMessage() {}
+
+func (x *GetMoreRepliesRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetMoreRepliesRequest.ProtoReflect.Descriptor instead.
+func (*GetMoreRepliesRequest) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *GetMoreRepliesRequest) GetRid() int64 {
+ if x != nil {
+ return x.Rid
+ }
+ return 0
+}
+
+func (x *GetMoreRepliesRequest) GetMaxId() int64 {
+ if x != nil {
+ return x.MaxId
+ }
+ return 0
+}
+
+func (x *GetMoreRepliesRequest) GetLimit() int64 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type GetMoreRepliesResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Replies []*Comment `protobuf:"bytes,1,rep,name=replies,proto3" json:"replies,omitempty"`
+}
+
+func (x *GetMoreRepliesResponse) Reset() {
+ *x = GetMoreRepliesResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetMoreRepliesResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetMoreRepliesResponse) ProtoMessage() {}
+
+func (x *GetMoreRepliesResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetMoreRepliesResponse.ProtoReflect.Descriptor instead.
+func (*GetMoreRepliesResponse) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetMoreRepliesResponse) GetReplies() []*Comment {
+ if x != nil {
+ return x.Replies
+ }
+ return nil
+}
+
+type Comment struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Uid int64 `protobuf:"varint,2,opt,name=uid,proto3" json:"uid,omitempty"`
+ Biz string `protobuf:"bytes,3,opt,name=biz,proto3" json:"biz,omitempty"`
+ Bizid int64 `protobuf:"varint,4,opt,name=bizid,proto3" json:"bizid,omitempty"`
+ Content string `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"`
+ // 这里你可以考虑,只传入 id
+ RootComment *Comment `protobuf:"bytes,6,opt,name=root_comment,json=rootComment,proto3" json:"root_comment,omitempty"`
+ // 只传入 id
+ ParentComment *Comment `protobuf:"bytes,7,opt,name=parent_comment,json=parentComment,proto3" json:"parent_comment,omitempty"`
+ // 正常来说,你在时间传递上,如果不想用 int64 之类的
+ // 就可以考虑使用这个 Timestamp
+ Ctime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=ctime,proto3" json:"ctime,omitempty"`
+ Utime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=utime,proto3" json:"utime,omitempty"`
+}
+
+func (x *Comment) Reset() {
+ *x = Comment{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_comment_v1_comment_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Comment) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Comment) ProtoMessage() {}
+
+func (x *Comment) ProtoReflect() protoreflect.Message {
+ mi := &file_comment_v1_comment_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Comment.ProtoReflect.Descriptor instead.
+func (*Comment) Descriptor() ([]byte, []int) {
+ return file_comment_v1_comment_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *Comment) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Comment) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *Comment) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *Comment) GetBizid() int64 {
+ if x != nil {
+ return x.Bizid
+ }
+ return 0
+}
+
+func (x *Comment) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *Comment) GetRootComment() *Comment {
+ if x != nil {
+ return x.RootComment
+ }
+ return nil
+}
+
+func (x *Comment) GetParentComment() *Comment {
+ if x != nil {
+ return x.ParentComment
+ }
+ return nil
+}
+
+func (x *Comment) GetCtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Ctime
+ }
+ return nil
+}
+
+func (x *Comment) GetUtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Utime
+ }
+ return nil
+}
+
+var File_comment_v1_comment_proto protoreflect.FileDescriptor
+
+var file_comment_v1_comment_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d,
+ 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x63, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x69, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x65,
+ 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
+ 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12,
+ 0x14, 0x0a, 0x05, 0x62, 0x69, 0x7a, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x62, 0x69, 0x7a, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05,
+ 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d,
+ 0x69, 0x74, 0x22, 0x46, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73,
+ 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x63, 0x6f, 0x6d,
+ 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f,
+ 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
+ 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02,
+ 0x69, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x0a, 0x14, 0x43,
+ 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76,
+ 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65,
+ 0x6e, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x56, 0x0a, 0x15, 0x47,
+ 0x65, 0x74, 0x4d, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x03, 0x72, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x64,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x12, 0x14, 0x0a,
+ 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69,
+ 0x6d, 0x69, 0x74, 0x22, 0x47, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x72, 0x65, 0x52, 0x65,
+ 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a,
+ 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13,
+ 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x22, 0xc5, 0x02, 0x0a,
+ 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69,
+ 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x14, 0x0a, 0x05,
+ 0x62, 0x69, 0x7a, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a,
+ 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x0c,
+ 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e,
+ 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x6f, 0x6d,
+ 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x63,
+ 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63,
+ 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
+ 0x74, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
+ 0x12, 0x30, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+ 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x63, 0x74, 0x69,
+ 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x75,
+ 0x74, 0x69, 0x6d, 0x65, 0x32, 0xe8, 0x02, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
+ 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6f,
+ 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69,
+ 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69,
+ 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0d, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f,
+ 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43,
+ 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e,
+ 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+ 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x54, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
+ 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43,
+ 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x72,
+ 0x65, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70,
+ 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x63, 0x6f,
+ 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x72, 0x65,
+ 0x52, 0x65, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+ 0xae, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x76, 0x31, 0x42, 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f,
+ 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65,
+ 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f,
+ 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x3b,
+ 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x58, 0x58, 0xaa,
+ 0x02, 0x0a, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0a, 0x43,
+ 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x16, 0x43, 0x6f, 0x6d, 0x6d,
+ 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
+ 0x74, 0x61, 0xea, 0x02, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x3a, 0x56, 0x31,
+ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_comment_v1_comment_proto_rawDescOnce sync.Once
+ file_comment_v1_comment_proto_rawDescData = file_comment_v1_comment_proto_rawDesc
+)
+
+func file_comment_v1_comment_proto_rawDescGZIP() []byte {
+ file_comment_v1_comment_proto_rawDescOnce.Do(func() {
+ file_comment_v1_comment_proto_rawDescData = protoimpl.X.CompressGZIP(file_comment_v1_comment_proto_rawDescData)
+ })
+ return file_comment_v1_comment_proto_rawDescData
+}
+
+var file_comment_v1_comment_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_comment_v1_comment_proto_goTypes = []interface{}{
+ (*CommentListRequest)(nil), // 0: comment.v1.CommentListRequest
+ (*CommentListResponse)(nil), // 1: comment.v1.CommentListResponse
+ (*DeleteCommentRequest)(nil), // 2: comment.v1.DeleteCommentRequest
+ (*DeleteCommentResponse)(nil), // 3: comment.v1.DeleteCommentResponse
+ (*CreateCommentRequest)(nil), // 4: comment.v1.CreateCommentRequest
+ (*CreateCommentResponse)(nil), // 5: comment.v1.CreateCommentResponse
+ (*GetMoreRepliesRequest)(nil), // 6: comment.v1.GetMoreRepliesRequest
+ (*GetMoreRepliesResponse)(nil), // 7: comment.v1.GetMoreRepliesResponse
+ (*Comment)(nil), // 8: comment.v1.Comment
+ (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
+}
+var file_comment_v1_comment_proto_depIdxs = []int32{
+ 8, // 0: comment.v1.CommentListResponse.comments:type_name -> comment.v1.Comment
+ 8, // 1: comment.v1.CreateCommentRequest.comment:type_name -> comment.v1.Comment
+ 8, // 2: comment.v1.GetMoreRepliesResponse.replies:type_name -> comment.v1.Comment
+ 8, // 3: comment.v1.Comment.root_comment:type_name -> comment.v1.Comment
+ 8, // 4: comment.v1.Comment.parent_comment:type_name -> comment.v1.Comment
+ 9, // 5: comment.v1.Comment.ctime:type_name -> google.protobuf.Timestamp
+ 9, // 6: comment.v1.Comment.utime:type_name -> google.protobuf.Timestamp
+ 0, // 7: comment.v1.CommentService.GetCommentList:input_type -> comment.v1.CommentListRequest
+ 2, // 8: comment.v1.CommentService.DeleteComment:input_type -> comment.v1.DeleteCommentRequest
+ 4, // 9: comment.v1.CommentService.CreateComment:input_type -> comment.v1.CreateCommentRequest
+ 6, // 10: comment.v1.CommentService.GetMoreReplies:input_type -> comment.v1.GetMoreRepliesRequest
+ 1, // 11: comment.v1.CommentService.GetCommentList:output_type -> comment.v1.CommentListResponse
+ 3, // 12: comment.v1.CommentService.DeleteComment:output_type -> comment.v1.DeleteCommentResponse
+ 5, // 13: comment.v1.CommentService.CreateComment:output_type -> comment.v1.CreateCommentResponse
+ 7, // 14: comment.v1.CommentService.GetMoreReplies:output_type -> comment.v1.GetMoreRepliesResponse
+ 11, // [11:15] is the sub-list for method output_type
+ 7, // [7:11] is the sub-list for method input_type
+ 7, // [7:7] is the sub-list for extension type_name
+ 7, // [7:7] is the sub-list for extension extendee
+ 0, // [0:7] is the sub-list for field type_name
+}
+
+func init() { file_comment_v1_comment_proto_init() }
+func file_comment_v1_comment_proto_init() {
+ if File_comment_v1_comment_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_comment_v1_comment_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CommentListRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CommentListResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DeleteCommentRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DeleteCommentResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateCommentRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateCommentResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetMoreRepliesRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetMoreRepliesResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_comment_v1_comment_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Comment); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_comment_v1_comment_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 9,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_comment_v1_comment_proto_goTypes,
+ DependencyIndexes: file_comment_v1_comment_proto_depIdxs,
+ MessageInfos: file_comment_v1_comment_proto_msgTypes,
+ }.Build()
+ File_comment_v1_comment_proto = out.File
+ file_comment_v1_comment_proto_rawDesc = nil
+ file_comment_v1_comment_proto_goTypes = nil
+ file_comment_v1_comment_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/comment/v1/comment_grpc.pb.go b/webook/api/proto/gen/comment/v1/comment_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..0f8467d13d97546edd69339856451ae846f91d85
--- /dev/null
+++ b/webook/api/proto/gen/comment/v1/comment_grpc.pb.go
@@ -0,0 +1,226 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: comment/v1/comment.proto
+
+package commentv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ CommentService_GetCommentList_FullMethodName = "/comment.v1.CommentService/GetCommentList"
+ CommentService_DeleteComment_FullMethodName = "/comment.v1.CommentService/DeleteComment"
+ CommentService_CreateComment_FullMethodName = "/comment.v1.CommentService/CreateComment"
+ CommentService_GetMoreReplies_FullMethodName = "/comment.v1.CommentService/GetMoreReplies"
+)
+
+// CommentServiceClient is the client API for CommentService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CommentServiceClient interface {
+ // GetCommentList Comment的id为0 获取一级评论
+ GetCommentList(ctx context.Context, in *CommentListRequest, opts ...grpc.CallOption) (*CommentListResponse, error)
+ // DeleteComment 删除评论,删除本评论和其子评论
+ DeleteComment(ctx context.Context, in *DeleteCommentRequest, opts ...grpc.CallOption) (*DeleteCommentResponse, error)
+ // CreateComment 创建评论
+ CreateComment(ctx context.Context, in *CreateCommentRequest, opts ...grpc.CallOption) (*CreateCommentResponse, error)
+ GetMoreReplies(ctx context.Context, in *GetMoreRepliesRequest, opts ...grpc.CallOption) (*GetMoreRepliesResponse, error)
+}
+
+type commentServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewCommentServiceClient(cc grpc.ClientConnInterface) CommentServiceClient {
+ return &commentServiceClient{cc}
+}
+
+func (c *commentServiceClient) GetCommentList(ctx context.Context, in *CommentListRequest, opts ...grpc.CallOption) (*CommentListResponse, error) {
+ out := new(CommentListResponse)
+ err := c.cc.Invoke(ctx, CommentService_GetCommentList_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *commentServiceClient) DeleteComment(ctx context.Context, in *DeleteCommentRequest, opts ...grpc.CallOption) (*DeleteCommentResponse, error) {
+ out := new(DeleteCommentResponse)
+ err := c.cc.Invoke(ctx, CommentService_DeleteComment_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *commentServiceClient) CreateComment(ctx context.Context, in *CreateCommentRequest, opts ...grpc.CallOption) (*CreateCommentResponse, error) {
+ out := new(CreateCommentResponse)
+ err := c.cc.Invoke(ctx, CommentService_CreateComment_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *commentServiceClient) GetMoreReplies(ctx context.Context, in *GetMoreRepliesRequest, opts ...grpc.CallOption) (*GetMoreRepliesResponse, error) {
+ out := new(GetMoreRepliesResponse)
+ err := c.cc.Invoke(ctx, CommentService_GetMoreReplies_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// CommentServiceServer is the server API for CommentService service.
+// All implementations must embed UnimplementedCommentServiceServer
+// for forward compatibility
+type CommentServiceServer interface {
+ // GetCommentList Comment的id为0 获取一级评论
+ GetCommentList(context.Context, *CommentListRequest) (*CommentListResponse, error)
+ // DeleteComment 删除评论,删除本评论和其子评论
+ DeleteComment(context.Context, *DeleteCommentRequest) (*DeleteCommentResponse, error)
+ // CreateComment 创建评论
+ CreateComment(context.Context, *CreateCommentRequest) (*CreateCommentResponse, error)
+ GetMoreReplies(context.Context, *GetMoreRepliesRequest) (*GetMoreRepliesResponse, error)
+ mustEmbedUnimplementedCommentServiceServer()
+}
+
+// UnimplementedCommentServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedCommentServiceServer struct {
+}
+
+func (UnimplementedCommentServiceServer) GetCommentList(context.Context, *CommentListRequest) (*CommentListResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetCommentList not implemented")
+}
+func (UnimplementedCommentServiceServer) DeleteComment(context.Context, *DeleteCommentRequest) (*DeleteCommentResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method DeleteComment not implemented")
+}
+func (UnimplementedCommentServiceServer) CreateComment(context.Context, *CreateCommentRequest) (*CreateCommentResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateComment not implemented")
+}
+func (UnimplementedCommentServiceServer) GetMoreReplies(context.Context, *GetMoreRepliesRequest) (*GetMoreRepliesResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetMoreReplies not implemented")
+}
+func (UnimplementedCommentServiceServer) mustEmbedUnimplementedCommentServiceServer() {}
+
+// UnsafeCommentServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CommentServiceServer will
+// result in compilation errors.
+type UnsafeCommentServiceServer interface {
+ mustEmbedUnimplementedCommentServiceServer()
+}
+
+func RegisterCommentServiceServer(s grpc.ServiceRegistrar, srv CommentServiceServer) {
+ s.RegisterService(&CommentService_ServiceDesc, srv)
+}
+
+func _CommentService_GetCommentList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CommentListRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CommentServiceServer).GetCommentList(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CommentService_GetCommentList_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CommentServiceServer).GetCommentList(ctx, req.(*CommentListRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CommentService_DeleteComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DeleteCommentRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CommentServiceServer).DeleteComment(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CommentService_DeleteComment_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CommentServiceServer).DeleteComment(ctx, req.(*DeleteCommentRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CommentService_CreateComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateCommentRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CommentServiceServer).CreateComment(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CommentService_CreateComment_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CommentServiceServer).CreateComment(ctx, req.(*CreateCommentRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CommentService_GetMoreReplies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetMoreRepliesRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CommentServiceServer).GetMoreReplies(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CommentService_GetMoreReplies_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CommentServiceServer).GetMoreReplies(ctx, req.(*GetMoreRepliesRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// CommentService_ServiceDesc is the grpc.ServiceDesc for CommentService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CommentService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "comment.v1.CommentService",
+ HandlerType: (*CommentServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "GetCommentList",
+ Handler: _CommentService_GetCommentList_Handler,
+ },
+ {
+ MethodName: "DeleteComment",
+ Handler: _CommentService_DeleteComment_Handler,
+ },
+ {
+ MethodName: "CreateComment",
+ Handler: _CommentService_CreateComment_Handler,
+ },
+ {
+ MethodName: "GetMoreReplies",
+ Handler: _CommentService_GetMoreReplies_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "comment/v1/comment.proto",
+}
diff --git a/webook/api/proto/gen/cronjob/v1/cronjob.pb.go b/webook/api/proto/gen/cronjob/v1/cronjob.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..1f0d3b0a263ca4fb1bca465f1e7425adb8bbcb9a
--- /dev/null
+++ b/webook/api/proto/gen/cronjob/v1/cronjob.pb.go
@@ -0,0 +1,580 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: cronjob/v1/cronjob.proto
+
+package cronjobv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CronJob struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+ Executor string `protobuf:"bytes,3,opt,name=executor,proto3" json:"executor,omitempty"`
+ Cfg string `protobuf:"bytes,4,opt,name=cfg,proto3" json:"cfg,omitempty"`
+ Expression string `protobuf:"bytes,5,opt,name=expression,proto3" json:"expression,omitempty"`
+ NextTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=next_time,json=nextTime,proto3" json:"next_time,omitempty"`
+}
+
+func (x *CronJob) Reset() {
+ *x = CronJob{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CronJob) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CronJob) ProtoMessage() {}
+
+func (x *CronJob) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CronJob.ProtoReflect.Descriptor instead.
+func (*CronJob) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CronJob) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *CronJob) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *CronJob) GetExecutor() string {
+ if x != nil {
+ return x.Executor
+ }
+ return ""
+}
+
+func (x *CronJob) GetCfg() string {
+ if x != nil {
+ return x.Cfg
+ }
+ return ""
+}
+
+func (x *CronJob) GetExpression() string {
+ if x != nil {
+ return x.Expression
+ }
+ return ""
+}
+
+func (x *CronJob) GetNextTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.NextTime
+ }
+ return nil
+}
+
+type PreemptResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Cronjob *CronJob `protobuf:"bytes,1,opt,name=cronjob,proto3" json:"cronjob,omitempty"`
+}
+
+func (x *PreemptResponse) Reset() {
+ *x = PreemptResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PreemptResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PreemptResponse) ProtoMessage() {}
+
+func (x *PreemptResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PreemptResponse.ProtoReflect.Descriptor instead.
+func (*PreemptResponse) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PreemptResponse) GetCronjob() *CronJob {
+ if x != nil {
+ return x.Cronjob
+ }
+ return nil
+}
+
+type PreemptRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *PreemptRequest) Reset() {
+ *x = PreemptRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PreemptRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PreemptRequest) ProtoMessage() {}
+
+func (x *PreemptRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PreemptRequest.ProtoReflect.Descriptor instead.
+func (*PreemptRequest) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{2}
+}
+
+type ResetNextTimeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Job *CronJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
+}
+
+func (x *ResetNextTimeRequest) Reset() {
+ *x = ResetNextTimeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ResetNextTimeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResetNextTimeRequest) ProtoMessage() {}
+
+func (x *ResetNextTimeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResetNextTimeRequest.ProtoReflect.Descriptor instead.
+func (*ResetNextTimeRequest) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ResetNextTimeRequest) GetJob() *CronJob {
+ if x != nil {
+ return x.Job
+ }
+ return nil
+}
+
+type ResetNextTimeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *ResetNextTimeResponse) Reset() {
+ *x = ResetNextTimeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ResetNextTimeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResetNextTimeResponse) ProtoMessage() {}
+
+func (x *ResetNextTimeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResetNextTimeResponse.ProtoReflect.Descriptor instead.
+func (*ResetNextTimeResponse) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{4}
+}
+
+type AddJobRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Job *CronJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
+}
+
+func (x *AddJobRequest) Reset() {
+ *x = AddJobRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AddJobRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddJobRequest) ProtoMessage() {}
+
+func (x *AddJobRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AddJobRequest.ProtoReflect.Descriptor instead.
+func (*AddJobRequest) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *AddJobRequest) GetJob() *CronJob {
+ if x != nil {
+ return x.Job
+ }
+ return nil
+}
+
+type AddJobResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *AddJobResponse) Reset() {
+ *x = AddJobResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AddJobResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddJobResponse) ProtoMessage() {}
+
+func (x *AddJobResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_cronjob_v1_cronjob_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AddJobResponse.ProtoReflect.Descriptor instead.
+func (*AddJobResponse) Descriptor() ([]byte, []int) {
+ return file_cronjob_v1_cronjob_proto_rawDescGZIP(), []int{6}
+}
+
+var File_cronjob_v1_cronjob_proto protoreflect.FileDescriptor
+
+var file_cronjob_v1_cronjob_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x72, 0x6f,
+ 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x63, 0x72, 0x6f, 0x6e,
+ 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb4, 0x01, 0x0a, 0x07, 0x43, 0x72, 0x6f, 0x6e,
+ 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x65, 0x63, 0x75,
+ 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x78, 0x65, 0x63, 0x75,
+ 0x74, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x66, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x03, 0x63, 0x66, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73,
+ 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65,
+ 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x74, 0x69,
+ 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+ 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
+ 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6e, 0x65, 0x78, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x40,
+ 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x65, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x2d, 0x0a, 0x07, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e,
+ 0x43, 0x72, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x07, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62,
+ 0x22, 0x10, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x65, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4e, 0x65, 0x78, 0x74, 0x54,
+ 0x69, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x03, 0x6a, 0x6f,
+ 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f,
+ 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f,
+ 0x62, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4e, 0x65, 0x78, 0x74, 0x54, 0x69,
+ 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x0d, 0x41, 0x64,
+ 0x64, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x03, 0x6a,
+ 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a,
+ 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a,
+ 0x6f, 0x62, 0x22, 0x10, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xf1, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x6f, 0x6e, 0x4a, 0x6f, 0x62,
+ 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x65, 0x6d,
+ 0x70, 0x74, 0x12, 0x1a, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e,
+ 0x50, 0x72, 0x65, 0x65, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
+ 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x65,
+ 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x56, 0x0a,
+ 0x0d, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4e, 0x65, 0x78, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x20,
+ 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x65,
+ 0x74, 0x4e, 0x65, 0x78, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x1a, 0x21, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65,
+ 0x73, 0x65, 0x74, 0x4e, 0x65, 0x78, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x06, 0x41, 0x64, 0x64, 0x4a, 0x6f, 0x62, 0x12,
+ 0x19, 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64,
+ 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x72, 0x6f,
+ 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x4a, 0x6f, 0x62, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xae, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d,
+ 0x2e, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x43, 0x72, 0x6f,
+ 0x6e, 0x6a, 0x6f, 0x62, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69, 0x74,
+ 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f,
+ 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f,
+ 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x72,
+ 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62,
+ 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x43, 0x72, 0x6f, 0x6e, 0x6a,
+ 0x6f, 0x62, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0a, 0x43, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x5c,
+ 0x56, 0x31, 0xe2, 0x02, 0x16, 0x43, 0x72, 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x5c, 0x56, 0x31, 0x5c,
+ 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0b, 0x43, 0x72,
+ 0x6f, 0x6e, 0x6a, 0x6f, 0x62, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
+}
+
+var (
+ file_cronjob_v1_cronjob_proto_rawDescOnce sync.Once
+ file_cronjob_v1_cronjob_proto_rawDescData = file_cronjob_v1_cronjob_proto_rawDesc
+)
+
+func file_cronjob_v1_cronjob_proto_rawDescGZIP() []byte {
+ file_cronjob_v1_cronjob_proto_rawDescOnce.Do(func() {
+ file_cronjob_v1_cronjob_proto_rawDescData = protoimpl.X.CompressGZIP(file_cronjob_v1_cronjob_proto_rawDescData)
+ })
+ return file_cronjob_v1_cronjob_proto_rawDescData
+}
+
+var file_cronjob_v1_cronjob_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+var file_cronjob_v1_cronjob_proto_goTypes = []interface{}{
+ (*CronJob)(nil), // 0: cronjob.v1.CronJob
+ (*PreemptResponse)(nil), // 1: cronjob.v1.PreemptResponse
+ (*PreemptRequest)(nil), // 2: cronjob.v1.PreemptRequest
+ (*ResetNextTimeRequest)(nil), // 3: cronjob.v1.ResetNextTimeRequest
+ (*ResetNextTimeResponse)(nil), // 4: cronjob.v1.ResetNextTimeResponse
+ (*AddJobRequest)(nil), // 5: cronjob.v1.AddJobRequest
+ (*AddJobResponse)(nil), // 6: cronjob.v1.AddJobResponse
+ (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp
+}
+var file_cronjob_v1_cronjob_proto_depIdxs = []int32{
+ 7, // 0: cronjob.v1.CronJob.next_time:type_name -> google.protobuf.Timestamp
+ 0, // 1: cronjob.v1.PreemptResponse.cronjob:type_name -> cronjob.v1.CronJob
+ 0, // 2: cronjob.v1.ResetNextTimeRequest.job:type_name -> cronjob.v1.CronJob
+ 0, // 3: cronjob.v1.AddJobRequest.job:type_name -> cronjob.v1.CronJob
+ 2, // 4: cronjob.v1.CronJobService.Preempt:input_type -> cronjob.v1.PreemptRequest
+ 3, // 5: cronjob.v1.CronJobService.ResetNextTime:input_type -> cronjob.v1.ResetNextTimeRequest
+ 5, // 6: cronjob.v1.CronJobService.AddJob:input_type -> cronjob.v1.AddJobRequest
+ 1, // 7: cronjob.v1.CronJobService.Preempt:output_type -> cronjob.v1.PreemptResponse
+ 4, // 8: cronjob.v1.CronJobService.ResetNextTime:output_type -> cronjob.v1.ResetNextTimeResponse
+ 6, // 9: cronjob.v1.CronJobService.AddJob:output_type -> cronjob.v1.AddJobResponse
+ 7, // [7:10] is the sub-list for method output_type
+ 4, // [4:7] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_cronjob_v1_cronjob_proto_init() }
+func file_cronjob_v1_cronjob_proto_init() {
+ if File_cronjob_v1_cronjob_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_cronjob_v1_cronjob_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CronJob); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PreemptResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PreemptRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ResetNextTimeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ResetNextTimeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AddJobRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_cronjob_v1_cronjob_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AddJobResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_cronjob_v1_cronjob_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 7,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_cronjob_v1_cronjob_proto_goTypes,
+ DependencyIndexes: file_cronjob_v1_cronjob_proto_depIdxs,
+ MessageInfos: file_cronjob_v1_cronjob_proto_msgTypes,
+ }.Build()
+ File_cronjob_v1_cronjob_proto = out.File
+ file_cronjob_v1_cronjob_proto_rawDesc = nil
+ file_cronjob_v1_cronjob_proto_goTypes = nil
+ file_cronjob_v1_cronjob_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/cronjob/v1/cronjob_grpc.pb.go b/webook/api/proto/gen/cronjob/v1/cronjob_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c98562d19a5a06597a14b324d4b04bf988b110b
--- /dev/null
+++ b/webook/api/proto/gen/cronjob/v1/cronjob_grpc.pb.go
@@ -0,0 +1,183 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: cronjob/v1/cronjob.proto
+
+package cronjobv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ CronJobService_Preempt_FullMethodName = "/cronjob.v1.CronJobService/Preempt"
+ CronJobService_ResetNextTime_FullMethodName = "/cronjob.v1.CronJobService/ResetNextTime"
+ CronJobService_AddJob_FullMethodName = "/cronjob.v1.CronJobService/AddJob"
+)
+
+// CronJobServiceClient is the client API for CronJobService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CronJobServiceClient interface {
+ Preempt(ctx context.Context, in *PreemptRequest, opts ...grpc.CallOption) (*PreemptResponse, error)
+ ResetNextTime(ctx context.Context, in *ResetNextTimeRequest, opts ...grpc.CallOption) (*ResetNextTimeResponse, error)
+ AddJob(ctx context.Context, in *AddJobRequest, opts ...grpc.CallOption) (*AddJobResponse, error)
+}
+
+type cronJobServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewCronJobServiceClient(cc grpc.ClientConnInterface) CronJobServiceClient {
+ return &cronJobServiceClient{cc}
+}
+
+func (c *cronJobServiceClient) Preempt(ctx context.Context, in *PreemptRequest, opts ...grpc.CallOption) (*PreemptResponse, error) {
+ out := new(PreemptResponse)
+ err := c.cc.Invoke(ctx, CronJobService_Preempt_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cronJobServiceClient) ResetNextTime(ctx context.Context, in *ResetNextTimeRequest, opts ...grpc.CallOption) (*ResetNextTimeResponse, error) {
+ out := new(ResetNextTimeResponse)
+ err := c.cc.Invoke(ctx, CronJobService_ResetNextTime_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cronJobServiceClient) AddJob(ctx context.Context, in *AddJobRequest, opts ...grpc.CallOption) (*AddJobResponse, error) {
+ out := new(AddJobResponse)
+ err := c.cc.Invoke(ctx, CronJobService_AddJob_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// CronJobServiceServer is the server API for CronJobService service.
+// All implementations must embed UnimplementedCronJobServiceServer
+// for forward compatibility
+type CronJobServiceServer interface {
+ Preempt(context.Context, *PreemptRequest) (*PreemptResponse, error)
+ ResetNextTime(context.Context, *ResetNextTimeRequest) (*ResetNextTimeResponse, error)
+ AddJob(context.Context, *AddJobRequest) (*AddJobResponse, error)
+ mustEmbedUnimplementedCronJobServiceServer()
+}
+
+// UnimplementedCronJobServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedCronJobServiceServer struct {
+}
+
+func (UnimplementedCronJobServiceServer) Preempt(context.Context, *PreemptRequest) (*PreemptResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Preempt not implemented")
+}
+func (UnimplementedCronJobServiceServer) ResetNextTime(context.Context, *ResetNextTimeRequest) (*ResetNextTimeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ResetNextTime not implemented")
+}
+func (UnimplementedCronJobServiceServer) AddJob(context.Context, *AddJobRequest) (*AddJobResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AddJob not implemented")
+}
+func (UnimplementedCronJobServiceServer) mustEmbedUnimplementedCronJobServiceServer() {}
+
+// UnsafeCronJobServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CronJobServiceServer will
+// result in compilation errors.
+type UnsafeCronJobServiceServer interface {
+ mustEmbedUnimplementedCronJobServiceServer()
+}
+
+func RegisterCronJobServiceServer(s grpc.ServiceRegistrar, srv CronJobServiceServer) {
+ s.RegisterService(&CronJobService_ServiceDesc, srv)
+}
+
+func _CronJobService_Preempt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(PreemptRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CronJobServiceServer).Preempt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CronJobService_Preempt_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CronJobServiceServer).Preempt(ctx, req.(*PreemptRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CronJobService_ResetNextTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ResetNextTimeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CronJobServiceServer).ResetNextTime(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CronJobService_ResetNextTime_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CronJobServiceServer).ResetNextTime(ctx, req.(*ResetNextTimeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CronJobService_AddJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AddJobRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CronJobServiceServer).AddJob(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CronJobService_AddJob_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CronJobServiceServer).AddJob(ctx, req.(*AddJobRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// CronJobService_ServiceDesc is the grpc.ServiceDesc for CronJobService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CronJobService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "cronjob.v1.CronJobService",
+ HandlerType: (*CronJobServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Preempt",
+ Handler: _CronJobService_Preempt_Handler,
+ },
+ {
+ MethodName: "ResetNextTime",
+ Handler: _CronJobService_ResetNextTime_Handler,
+ },
+ {
+ MethodName: "AddJob",
+ Handler: _CronJobService_AddJob_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "cronjob/v1/cronjob.proto",
+}
diff --git a/webook/api/proto/gen/feed/v1/feed.pb.go b/webook/api/proto/gen/feed/v1/feed.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..a9a72f1021e9f84296bd15c93689340aa86ec229
--- /dev/null
+++ b/webook/api/proto/gen/feed/v1/feed.pb.go
@@ -0,0 +1,586 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.31.0
+// protoc v3.21.12
+// source: feed/v1/feed.proto
+
+package feedv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type User struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *User) Reset() {
+ *x = User{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *User) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*User) ProtoMessage() {}
+
+func (x *User) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use User.ProtoReflect.Descriptor instead.
+func (*User) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *User) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type Article struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *Article) Reset() {
+ *x = Article{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Article) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *Article) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type FeedEvent struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ User *User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
+ Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
+ Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+ Ctime int64 `protobuf:"varint,5,opt,name=ctime,proto3" json:"ctime,omitempty"`
+}
+
+func (x *FeedEvent) Reset() {
+ *x = FeedEvent{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FeedEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FeedEvent) ProtoMessage() {}
+
+func (x *FeedEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FeedEvent.ProtoReflect.Descriptor instead.
+func (*FeedEvent) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FeedEvent) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *FeedEvent) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+func (x *FeedEvent) GetType() string {
+ if x != nil {
+ return x.Type
+ }
+ return ""
+}
+
+func (x *FeedEvent) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *FeedEvent) GetCtime() int64 {
+ if x != nil {
+ return x.Ctime
+ }
+ return 0
+}
+
+type CreateFeedEventRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FeedEvent *FeedEvent `protobuf:"bytes,1,opt,name=feedEvent,proto3" json:"feedEvent,omitempty"`
+}
+
+func (x *CreateFeedEventRequest) Reset() {
+ *x = CreateFeedEventRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateFeedEventRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateFeedEventRequest) ProtoMessage() {}
+
+func (x *CreateFeedEventRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateFeedEventRequest.ProtoReflect.Descriptor instead.
+func (*CreateFeedEventRequest) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CreateFeedEventRequest) GetFeedEvent() *FeedEvent {
+ if x != nil {
+ return x.FeedEvent
+ }
+ return nil
+}
+
+type CreateFeedEventResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CreateFeedEventResponse) Reset() {
+ *x = CreateFeedEventResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateFeedEventResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateFeedEventResponse) ProtoMessage() {}
+
+func (x *CreateFeedEventResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateFeedEventResponse.ProtoReflect.Descriptor instead.
+func (*CreateFeedEventResponse) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{4}
+}
+
+type FindFeedEventsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Uid int64 `protobuf:"varint,1,opt,name=Uid,proto3" json:"Uid,omitempty"`
+ Limit int64 `protobuf:"varint,2,opt,name=Limit,proto3" json:"Limit,omitempty"`
+ Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+}
+
+func (x *FindFeedEventsRequest) Reset() {
+ *x = FindFeedEventsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindFeedEventsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindFeedEventsRequest) ProtoMessage() {}
+
+func (x *FindFeedEventsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindFeedEventsRequest.ProtoReflect.Descriptor instead.
+func (*FindFeedEventsRequest) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *FindFeedEventsRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *FindFeedEventsRequest) GetLimit() int64 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+func (x *FindFeedEventsRequest) GetTimestamp() int64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
+type FindFeedEventsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FeedEvents []*FeedEvent `protobuf:"bytes,1,rep,name=feedEvents,proto3" json:"feedEvents,omitempty"`
+}
+
+func (x *FindFeedEventsResponse) Reset() {
+ *x = FindFeedEventsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_feed_v1_feed_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindFeedEventsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindFeedEventsResponse) ProtoMessage() {}
+
+func (x *FindFeedEventsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_feed_v1_feed_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindFeedEventsResponse.ProtoReflect.Descriptor instead.
+func (*FindFeedEventsResponse) Descriptor() ([]byte, []int) {
+ return file_feed_v1_feed_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *FindFeedEventsResponse) GetFeedEvents() []*FeedEvent {
+ if x != nil {
+ return x.FeedEvents
+ }
+ return nil
+}
+
+var File_feed_v1_feed_proto protoreflect.FileDescriptor
+
+var file_feed_v1_feed_proto_rawDesc = []byte{
+ 0x0a, 0x12, 0x66, 0x65, 0x65, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x76, 0x31, 0x22, 0x16, 0x0a,
+ 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x19, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64,
+ 0x22, 0x82, 0x01, 0x0a, 0x09, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e,
+ 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21,
+ 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x66,
+ 0x65, 0x65, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65,
+ 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12,
+ 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x63, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x4a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46,
+ 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x30, 0x0a, 0x09, 0x66, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x65,
+ 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x09, 0x66, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x22, 0x19, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x64, 0x45,
+ 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5d, 0x0a, 0x15,
+ 0x46, 0x69, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x03, 0x55, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x69, 0x6d, 0x69, 0x74,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1c, 0x0a,
+ 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x4c, 0x0a, 0x16, 0x46,
+ 0x69, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65,
+ 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x65, 0x64,
+ 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x66,
+ 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x32, 0xb2, 0x01, 0x0a, 0x07, 0x46, 0x65,
+ 0x65, 0x64, 0x53, 0x76, 0x63, 0x12, 0x54, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46,
+ 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1f, 0x2e, 0x66, 0x65, 0x65, 0x64, 0x2e,
+ 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65,
+ 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x66, 0x65, 0x65, 0x64,
+ 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76,
+ 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0e, 0x46,
+ 0x69, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1e, 0x2e,
+ 0x66, 0x65, 0x65, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x64,
+ 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e,
+ 0x66, 0x65, 0x65, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x64,
+ 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10,
+ 0x5a, 0x0e, 0x66, 0x65, 0x65, 0x64, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x65, 0x65, 0x64, 0x76, 0x31,
+ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_feed_v1_feed_proto_rawDescOnce sync.Once
+ file_feed_v1_feed_proto_rawDescData = file_feed_v1_feed_proto_rawDesc
+)
+
+func file_feed_v1_feed_proto_rawDescGZIP() []byte {
+ file_feed_v1_feed_proto_rawDescOnce.Do(func() {
+ file_feed_v1_feed_proto_rawDescData = protoimpl.X.CompressGZIP(file_feed_v1_feed_proto_rawDescData)
+ })
+ return file_feed_v1_feed_proto_rawDescData
+}
+
+var file_feed_v1_feed_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+var file_feed_v1_feed_proto_goTypes = []interface{}{
+ (*User)(nil), // 0: feed.v1.User
+ (*Article)(nil), // 1: feed.v1.Article
+ (*FeedEvent)(nil), // 2: feed.v1.FeedEvent
+ (*CreateFeedEventRequest)(nil), // 3: feed.v1.CreateFeedEventRequest
+ (*CreateFeedEventResponse)(nil), // 4: feed.v1.CreateFeedEventResponse
+ (*FindFeedEventsRequest)(nil), // 5: feed.v1.FindFeedEventsRequest
+ (*FindFeedEventsResponse)(nil), // 6: feed.v1.FindFeedEventsResponse
+}
+var file_feed_v1_feed_proto_depIdxs = []int32{
+ 0, // 0: feed.v1.FeedEvent.user:type_name -> feed.v1.User
+ 2, // 1: feed.v1.CreateFeedEventRequest.feedEvent:type_name -> feed.v1.FeedEvent
+ 2, // 2: feed.v1.FindFeedEventsResponse.feedEvents:type_name -> feed.v1.FeedEvent
+ 3, // 3: feed.v1.FeedSvc.CreateFeedEvent:input_type -> feed.v1.CreateFeedEventRequest
+ 5, // 4: feed.v1.FeedSvc.FindFeedEvents:input_type -> feed.v1.FindFeedEventsRequest
+ 4, // 5: feed.v1.FeedSvc.CreateFeedEvent:output_type -> feed.v1.CreateFeedEventResponse
+ 6, // 6: feed.v1.FeedSvc.FindFeedEvents:output_type -> feed.v1.FindFeedEventsResponse
+ 5, // [5:7] is the sub-list for method output_type
+ 3, // [3:5] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_feed_v1_feed_proto_init() }
+func file_feed_v1_feed_proto_init() {
+ if File_feed_v1_feed_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_feed_v1_feed_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*User); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Article); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FeedEvent); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateFeedEventRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateFeedEventResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindFeedEventsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_feed_v1_feed_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindFeedEventsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_feed_v1_feed_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 7,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_feed_v1_feed_proto_goTypes,
+ DependencyIndexes: file_feed_v1_feed_proto_depIdxs,
+ MessageInfos: file_feed_v1_feed_proto_msgTypes,
+ }.Build()
+ File_feed_v1_feed_proto = out.File
+ file_feed_v1_feed_proto_rawDesc = nil
+ file_feed_v1_feed_proto_goTypes = nil
+ file_feed_v1_feed_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/feed/v1/feed_grpc.pb.go b/webook/api/proto/gen/feed/v1/feed_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..467605c264a10701f12ebc5cec981a76e6711af7
--- /dev/null
+++ b/webook/api/proto/gen/feed/v1/feed_grpc.pb.go
@@ -0,0 +1,146 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v3.21.12
+// source: feed/v1/feed.proto
+
+package feedv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ FeedSvc_CreateFeedEvent_FullMethodName = "/feed.v1.FeedSvc/CreateFeedEvent"
+ FeedSvc_FindFeedEvents_FullMethodName = "/feed.v1.FeedSvc/FindFeedEvents"
+)
+
+// FeedSvcClient is the client API for FeedSvc service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type FeedSvcClient interface {
+ CreateFeedEvent(ctx context.Context, in *CreateFeedEventRequest, opts ...grpc.CallOption) (*CreateFeedEventResponse, error)
+ FindFeedEvents(ctx context.Context, in *FindFeedEventsRequest, opts ...grpc.CallOption) (*FindFeedEventsResponse, error)
+}
+
+type feedSvcClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewFeedSvcClient(cc grpc.ClientConnInterface) FeedSvcClient {
+ return &feedSvcClient{cc}
+}
+
+func (c *feedSvcClient) CreateFeedEvent(ctx context.Context, in *CreateFeedEventRequest, opts ...grpc.CallOption) (*CreateFeedEventResponse, error) {
+ out := new(CreateFeedEventResponse)
+ err := c.cc.Invoke(ctx, FeedSvc_CreateFeedEvent_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *feedSvcClient) FindFeedEvents(ctx context.Context, in *FindFeedEventsRequest, opts ...grpc.CallOption) (*FindFeedEventsResponse, error) {
+ out := new(FindFeedEventsResponse)
+ err := c.cc.Invoke(ctx, FeedSvc_FindFeedEvents_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// FeedSvcServer is the server API for FeedSvc service.
+// All implementations must embed UnimplementedFeedSvcServer
+// for forward compatibility
+type FeedSvcServer interface {
+ CreateFeedEvent(context.Context, *CreateFeedEventRequest) (*CreateFeedEventResponse, error)
+ FindFeedEvents(context.Context, *FindFeedEventsRequest) (*FindFeedEventsResponse, error)
+ mustEmbedUnimplementedFeedSvcServer()
+}
+
+// UnimplementedFeedSvcServer must be embedded to have forward compatible implementations.
+type UnimplementedFeedSvcServer struct {
+}
+
+func (UnimplementedFeedSvcServer) CreateFeedEvent(context.Context, *CreateFeedEventRequest) (*CreateFeedEventResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateFeedEvent not implemented")
+}
+func (UnimplementedFeedSvcServer) FindFeedEvents(context.Context, *FindFeedEventsRequest) (*FindFeedEventsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FindFeedEvents not implemented")
+}
+func (UnimplementedFeedSvcServer) mustEmbedUnimplementedFeedSvcServer() {}
+
+// UnsafeFeedSvcServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to FeedSvcServer will
+// result in compilation errors.
+type UnsafeFeedSvcServer interface {
+ mustEmbedUnimplementedFeedSvcServer()
+}
+
+func RegisterFeedSvcServer(s grpc.ServiceRegistrar, srv FeedSvcServer) {
+ s.RegisterService(&FeedSvc_ServiceDesc, srv)
+}
+
+func _FeedSvc_CreateFeedEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateFeedEventRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FeedSvcServer).CreateFeedEvent(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FeedSvc_CreateFeedEvent_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FeedSvcServer).CreateFeedEvent(ctx, req.(*CreateFeedEventRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FeedSvc_FindFeedEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FindFeedEventsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FeedSvcServer).FindFeedEvents(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FeedSvc_FindFeedEvents_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FeedSvcServer).FindFeedEvents(ctx, req.(*FindFeedEventsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// FeedSvc_ServiceDesc is the grpc.ServiceDesc for FeedSvc service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var FeedSvc_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "feed.v1.FeedSvc",
+ HandlerType: (*FeedSvcServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "CreateFeedEvent",
+ Handler: _FeedSvc_CreateFeedEvent_Handler,
+ },
+ {
+ MethodName: "FindFeedEvents",
+ Handler: _FeedSvc_FindFeedEvents_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "feed/v1/feed.proto",
+}
diff --git a/webook/api/proto/gen/follow/v1/follow.pb.go b/webook/api/proto/gen/follow/v1/follow.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..05259cb7020c5f4c5aabbca811ed47b350071f1d
--- /dev/null
+++ b/webook/api/proto/gen/follow/v1/follow.pb.go
@@ -0,0 +1,1094 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: follow/v1/follow.proto
+
+package followv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type FollowRelation struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Follower int64 `protobuf:"varint,2,opt,name=follower,proto3" json:"follower,omitempty"`
+ Followee int64 `protobuf:"varint,3,opt,name=followee,proto3" json:"followee,omitempty"`
+}
+
+func (x *FollowRelation) Reset() {
+ *x = FollowRelation{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowRelation) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowRelation) ProtoMessage() {}
+
+func (x *FollowRelation) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowRelation.ProtoReflect.Descriptor instead.
+func (*FollowRelation) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FollowRelation) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *FollowRelation) GetFollower() int64 {
+ if x != nil {
+ return x.Follower
+ }
+ return 0
+}
+
+func (x *FollowRelation) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+type FollowStatic struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 被多少人关注
+ Followers int64 `protobuf:"varint,1,opt,name=followers,proto3" json:"followers,omitempty"`
+ // 自己关注了多少人
+ Followees int64 `protobuf:"varint,2,opt,name=followees,proto3" json:"followees,omitempty"`
+}
+
+func (x *FollowStatic) Reset() {
+ *x = FollowStatic{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowStatic) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowStatic) ProtoMessage() {}
+
+func (x *FollowStatic) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowStatic.ProtoReflect.Descriptor instead.
+func (*FollowStatic) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *FollowStatic) GetFollowers() int64 {
+ if x != nil {
+ return x.Followers
+ }
+ return 0
+}
+
+func (x *FollowStatic) GetFollowees() int64 {
+ if x != nil {
+ return x.Followees
+ }
+ return 0
+}
+
+type GetFollowStaticRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Followee int64 `protobuf:"varint,1,opt,name=followee,proto3" json:"followee,omitempty"`
+}
+
+func (x *GetFollowStaticRequest) Reset() {
+ *x = GetFollowStaticRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFollowStaticRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFollowStaticRequest) ProtoMessage() {}
+
+func (x *GetFollowStaticRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFollowStaticRequest.ProtoReflect.Descriptor instead.
+func (*GetFollowStaticRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *GetFollowStaticRequest) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+type GetFollowStaticResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FollowStatic *FollowStatic `protobuf:"bytes,1,opt,name=followStatic,proto3" json:"followStatic,omitempty"`
+}
+
+func (x *GetFollowStaticResponse) Reset() {
+ *x = GetFollowStaticResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFollowStaticResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFollowStaticResponse) ProtoMessage() {}
+
+func (x *GetFollowStaticResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFollowStaticResponse.ProtoReflect.Descriptor instead.
+func (*GetFollowStaticResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GetFollowStaticResponse) GetFollowStatic() *FollowStatic {
+ if x != nil {
+ return x.FollowStatic
+ }
+ return nil
+}
+
+type GetFolloweeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 关注者,也就是某人查看自己的关注列表
+ Follower int64 `protobuf:"varint,1,opt,name=follower,proto3" json:"follower,omitempty"`
+ // min_id, max_id
+ Offset int64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
+ Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
+}
+
+func (x *GetFolloweeRequest) Reset() {
+ *x = GetFolloweeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFolloweeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFolloweeRequest) ProtoMessage() {}
+
+func (x *GetFolloweeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFolloweeRequest.ProtoReflect.Descriptor instead.
+func (*GetFolloweeRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetFolloweeRequest) GetFollower() int64 {
+ if x != nil {
+ return x.Follower
+ }
+ return 0
+}
+
+func (x *GetFolloweeRequest) GetOffset() int64 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+func (x *GetFolloweeRequest) GetLimit() int64 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type GetFolloweeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FollowRelations []*FollowRelation `protobuf:"bytes,1,rep,name=follow_relations,json=followRelations,proto3" json:"follow_relations,omitempty"`
+}
+
+func (x *GetFolloweeResponse) Reset() {
+ *x = GetFolloweeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFolloweeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFolloweeResponse) ProtoMessage() {}
+
+func (x *GetFolloweeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFolloweeResponse.ProtoReflect.Descriptor instead.
+func (*GetFolloweeResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetFolloweeResponse) GetFollowRelations() []*FollowRelation {
+ if x != nil {
+ return x.FollowRelations
+ }
+ return nil
+}
+
+type FollowInfoRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 关注者
+ Follower int64 `protobuf:"varint,1,opt,name=follower,proto3" json:"follower,omitempty"`
+ // 被关注者
+ Followee int64 `protobuf:"varint,2,opt,name=followee,proto3" json:"followee,omitempty"`
+}
+
+func (x *FollowInfoRequest) Reset() {
+ *x = FollowInfoRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowInfoRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowInfoRequest) ProtoMessage() {}
+
+func (x *FollowInfoRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowInfoRequest.ProtoReflect.Descriptor instead.
+func (*FollowInfoRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *FollowInfoRequest) GetFollower() int64 {
+ if x != nil {
+ return x.Follower
+ }
+ return 0
+}
+
+func (x *FollowInfoRequest) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+type FollowInfoResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FollowRelation *FollowRelation `protobuf:"bytes,1,opt,name=follow_relation,json=followRelation,proto3" json:"follow_relation,omitempty"`
+}
+
+func (x *FollowInfoResponse) Reset() {
+ *x = FollowInfoResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowInfoResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowInfoResponse) ProtoMessage() {}
+
+func (x *FollowInfoResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowInfoResponse.ProtoReflect.Descriptor instead.
+func (*FollowInfoResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *FollowInfoResponse) GetFollowRelation() *FollowRelation {
+ if x != nil {
+ return x.FollowRelation
+ }
+ return nil
+}
+
+type FollowRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 被关注者
+ Followee int64 `protobuf:"varint,1,opt,name=followee,proto3" json:"followee,omitempty"`
+ // 关注者
+ Follower int64 `protobuf:"varint,2,opt,name=follower,proto3" json:"follower,omitempty"`
+}
+
+func (x *FollowRequest) Reset() {
+ *x = FollowRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowRequest) ProtoMessage() {}
+
+func (x *FollowRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowRequest.ProtoReflect.Descriptor instead.
+func (*FollowRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *FollowRequest) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+func (x *FollowRequest) GetFollower() int64 {
+ if x != nil {
+ return x.Follower
+ }
+ return 0
+}
+
+type FollowResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *FollowResponse) Reset() {
+ *x = FollowResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FollowResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FollowResponse) ProtoMessage() {}
+
+func (x *FollowResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FollowResponse.ProtoReflect.Descriptor instead.
+func (*FollowResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{9}
+}
+
+type CancelFollowRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 被关注者
+ Followee int64 `protobuf:"varint,1,opt,name=followee,proto3" json:"followee,omitempty"`
+ // 关注者
+ Follower int64 `protobuf:"varint,2,opt,name=follower,proto3" json:"follower,omitempty"`
+}
+
+func (x *CancelFollowRequest) Reset() {
+ *x = CancelFollowRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelFollowRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelFollowRequest) ProtoMessage() {}
+
+func (x *CancelFollowRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelFollowRequest.ProtoReflect.Descriptor instead.
+func (*CancelFollowRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *CancelFollowRequest) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+func (x *CancelFollowRequest) GetFollower() int64 {
+ if x != nil {
+ return x.Follower
+ }
+ return 0
+}
+
+type CancelFollowResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CancelFollowResponse) Reset() {
+ *x = CancelFollowResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelFollowResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelFollowResponse) ProtoMessage() {}
+
+func (x *CancelFollowResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[11]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelFollowResponse.ProtoReflect.Descriptor instead.
+func (*CancelFollowResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{11}
+}
+
+type GetFollowerRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Followee int64 `protobuf:"varint,1,opt,name=followee,proto3" json:"followee,omitempty"`
+}
+
+func (x *GetFollowerRequest) Reset() {
+ *x = GetFollowerRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFollowerRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFollowerRequest) ProtoMessage() {}
+
+func (x *GetFollowerRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[12]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFollowerRequest.ProtoReflect.Descriptor instead.
+func (*GetFollowerRequest) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *GetFollowerRequest) GetFollowee() int64 {
+ if x != nil {
+ return x.Followee
+ }
+ return 0
+}
+
+type GetFollowerResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FollowRelations []*FollowRelation `protobuf:"bytes,1,rep,name=follow_relations,json=followRelations,proto3" json:"follow_relations,omitempty"`
+}
+
+func (x *GetFollowerResponse) Reset() {
+ *x = GetFollowerResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_follow_v1_follow_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetFollowerResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetFollowerResponse) ProtoMessage() {}
+
+func (x *GetFollowerResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_follow_v1_follow_proto_msgTypes[13]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetFollowerResponse.ProtoReflect.Descriptor instead.
+func (*GetFollowerResponse) Descriptor() ([]byte, []int) {
+ return file_follow_v1_follow_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *GetFollowerResponse) GetFollowRelations() []*FollowRelation {
+ if x != nil {
+ return x.FollowRelations
+ }
+ return nil
+}
+
+var File_follow_v1_follow_proto protoreflect.FileDescriptor
+
+var file_follow_v1_follow_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2f, 0x76, 0x31, 0x2f, 0x66, 0x6f, 0x6c, 0x6c,
+ 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77,
+ 0x2e, 0x76, 0x31, 0x22, 0x58, 0x0a, 0x0e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
+ 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
+ 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x22, 0x4a, 0x0a,
+ 0x0c, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x12, 0x1c, 0x0a,
+ 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x66,
+ 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x22, 0x34, 0x0a, 0x16, 0x47, 0x65, 0x74,
+ 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x22,
+ 0x56, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74,
+ 0x69, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0c, 0x66, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x17, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x52, 0x0c, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x22, 0x5e, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x46, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
+ 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66,
+ 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65,
+ 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x46, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44,
+ 0x0a, 0x10, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f,
+ 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4b, 0x0a, 0x11, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e,
+ 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
+ 0x65, 0x22, 0x58, 0x0a, 0x12, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x66, 0x6f, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0f, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x19, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x47, 0x0a, 0x0d, 0x46,
+ 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c,
+ 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c,
+ 0x6f, 0x77, 0x65, 0x72, 0x22, 0x10, 0x0a, 0x0e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c,
+ 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
+ 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x16, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x46,
+ 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x0a,
+ 0x12, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x22,
+ 0x5b, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77,
+ 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x19, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0xe0, 0x03, 0x0a,
+ 0x0d, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d,
+ 0x0a, 0x06, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x18, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46,
+ 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a,
+ 0x0c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x2e,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c,
+ 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c,
+ 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c,
+ 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x12, 0x1d, 0x2e,
+ 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x66,
+ 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c,
+ 0x6f, 0x77, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a,
+ 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x66,
+ 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x66, 0x6f, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x46, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x1d, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76,
+ 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c,
+ 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x12, 0x21, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x74,
+ 0x61, 0x74, 0x69, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x66, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+ 0xa6, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x76,
+ 0x31, 0x42, 0x0b, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
+ 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b,
+ 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65,
+ 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67,
+ 0x65, 0x6e, 0x2f, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6f, 0x6c,
+ 0x6c, 0x6f, 0x77, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x46, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x46, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x09, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77,
+ 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5c, 0x56, 0x31, 0x5c,
+ 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x46, 0x6f,
+ 0x6c, 0x6c, 0x6f, 0x77, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_follow_v1_follow_proto_rawDescOnce sync.Once
+ file_follow_v1_follow_proto_rawDescData = file_follow_v1_follow_proto_rawDesc
+)
+
+func file_follow_v1_follow_proto_rawDescGZIP() []byte {
+ file_follow_v1_follow_proto_rawDescOnce.Do(func() {
+ file_follow_v1_follow_proto_rawDescData = protoimpl.X.CompressGZIP(file_follow_v1_follow_proto_rawDescData)
+ })
+ return file_follow_v1_follow_proto_rawDescData
+}
+
+var file_follow_v1_follow_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_follow_v1_follow_proto_goTypes = []interface{}{
+ (*FollowRelation)(nil), // 0: follow.v1.FollowRelation
+ (*FollowStatic)(nil), // 1: follow.v1.FollowStatic
+ (*GetFollowStaticRequest)(nil), // 2: follow.v1.GetFollowStaticRequest
+ (*GetFollowStaticResponse)(nil), // 3: follow.v1.GetFollowStaticResponse
+ (*GetFolloweeRequest)(nil), // 4: follow.v1.GetFolloweeRequest
+ (*GetFolloweeResponse)(nil), // 5: follow.v1.GetFolloweeResponse
+ (*FollowInfoRequest)(nil), // 6: follow.v1.FollowInfoRequest
+ (*FollowInfoResponse)(nil), // 7: follow.v1.FollowInfoResponse
+ (*FollowRequest)(nil), // 8: follow.v1.FollowRequest
+ (*FollowResponse)(nil), // 9: follow.v1.FollowResponse
+ (*CancelFollowRequest)(nil), // 10: follow.v1.CancelFollowRequest
+ (*CancelFollowResponse)(nil), // 11: follow.v1.CancelFollowResponse
+ (*GetFollowerRequest)(nil), // 12: follow.v1.GetFollowerRequest
+ (*GetFollowerResponse)(nil), // 13: follow.v1.GetFollowerResponse
+}
+var file_follow_v1_follow_proto_depIdxs = []int32{
+ 1, // 0: follow.v1.GetFollowStaticResponse.followStatic:type_name -> follow.v1.FollowStatic
+ 0, // 1: follow.v1.GetFolloweeResponse.follow_relations:type_name -> follow.v1.FollowRelation
+ 0, // 2: follow.v1.FollowInfoResponse.follow_relation:type_name -> follow.v1.FollowRelation
+ 0, // 3: follow.v1.GetFollowerResponse.follow_relations:type_name -> follow.v1.FollowRelation
+ 8, // 4: follow.v1.FollowService.Follow:input_type -> follow.v1.FollowRequest
+ 10, // 5: follow.v1.FollowService.CancelFollow:input_type -> follow.v1.CancelFollowRequest
+ 4, // 6: follow.v1.FollowService.GetFollowee:input_type -> follow.v1.GetFolloweeRequest
+ 6, // 7: follow.v1.FollowService.FollowInfo:input_type -> follow.v1.FollowInfoRequest
+ 12, // 8: follow.v1.FollowService.GetFollower:input_type -> follow.v1.GetFollowerRequest
+ 2, // 9: follow.v1.FollowService.GetFollowStatic:input_type -> follow.v1.GetFollowStaticRequest
+ 9, // 10: follow.v1.FollowService.Follow:output_type -> follow.v1.FollowResponse
+ 11, // 11: follow.v1.FollowService.CancelFollow:output_type -> follow.v1.CancelFollowResponse
+ 5, // 12: follow.v1.FollowService.GetFollowee:output_type -> follow.v1.GetFolloweeResponse
+ 7, // 13: follow.v1.FollowService.FollowInfo:output_type -> follow.v1.FollowInfoResponse
+ 13, // 14: follow.v1.FollowService.GetFollower:output_type -> follow.v1.GetFollowerResponse
+ 3, // 15: follow.v1.FollowService.GetFollowStatic:output_type -> follow.v1.GetFollowStaticResponse
+ 10, // [10:16] is the sub-list for method output_type
+ 4, // [4:10] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_follow_v1_follow_proto_init() }
+func file_follow_v1_follow_proto_init() {
+ if File_follow_v1_follow_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_follow_v1_follow_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowRelation); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowStatic); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFollowStaticRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFollowStaticResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFolloweeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFolloweeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowInfoRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowInfoResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FollowResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelFollowRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelFollowResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFollowerRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_follow_v1_follow_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetFollowerResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_follow_v1_follow_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 14,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_follow_v1_follow_proto_goTypes,
+ DependencyIndexes: file_follow_v1_follow_proto_depIdxs,
+ MessageInfos: file_follow_v1_follow_proto_msgTypes,
+ }.Build()
+ File_follow_v1_follow_proto = out.File
+ file_follow_v1_follow_proto_rawDesc = nil
+ file_follow_v1_follow_proto_goTypes = nil
+ file_follow_v1_follow_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/follow/v1/follow_grpc.pb.go b/webook/api/proto/gen/follow/v1/follow_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..24a6728f5c0159a87ccb5b2c0b3e68952b97cc4a
--- /dev/null
+++ b/webook/api/proto/gen/follow/v1/follow_grpc.pb.go
@@ -0,0 +1,304 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: follow/v1/follow.proto
+
+package followv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ FollowService_Follow_FullMethodName = "/follow.v1.FollowService/Follow"
+ FollowService_CancelFollow_FullMethodName = "/follow.v1.FollowService/CancelFollow"
+ FollowService_GetFollowee_FullMethodName = "/follow.v1.FollowService/GetFollowee"
+ FollowService_FollowInfo_FullMethodName = "/follow.v1.FollowService/FollowInfo"
+ FollowService_GetFollower_FullMethodName = "/follow.v1.FollowService/GetFollower"
+ FollowService_GetFollowStatic_FullMethodName = "/follow.v1.FollowService/GetFollowStatic"
+)
+
+// FollowServiceClient is the client API for FollowService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type FollowServiceClient interface {
+ // 增删
+ Follow(ctx context.Context, in *FollowRequest, opts ...grpc.CallOption) (*FollowResponse, error)
+ CancelFollow(ctx context.Context, in *CancelFollowRequest, opts ...grpc.CallOption) (*CancelFollowResponse, error)
+ // 获得某个人的关注列表
+ GetFollowee(ctx context.Context, in *GetFolloweeRequest, opts ...grpc.CallOption) (*GetFolloweeResponse, error)
+ // 获得某个人关注另外一个人的详细信息
+ FollowInfo(ctx context.Context, in *FollowInfoRequest, opts ...grpc.CallOption) (*FollowInfoResponse, error)
+ // 获取某人的粉丝列表
+ GetFollower(ctx context.Context, in *GetFollowerRequest, opts ...grpc.CallOption) (*GetFollowerResponse, error)
+ // 获取默认的关注人数
+ GetFollowStatic(ctx context.Context, in *GetFollowStaticRequest, opts ...grpc.CallOption) (*GetFollowStaticResponse, error)
+}
+
+type followServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewFollowServiceClient(cc grpc.ClientConnInterface) FollowServiceClient {
+ return &followServiceClient{cc}
+}
+
+func (c *followServiceClient) Follow(ctx context.Context, in *FollowRequest, opts ...grpc.CallOption) (*FollowResponse, error) {
+ out := new(FollowResponse)
+ err := c.cc.Invoke(ctx, FollowService_Follow_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *followServiceClient) CancelFollow(ctx context.Context, in *CancelFollowRequest, opts ...grpc.CallOption) (*CancelFollowResponse, error) {
+ out := new(CancelFollowResponse)
+ err := c.cc.Invoke(ctx, FollowService_CancelFollow_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *followServiceClient) GetFollowee(ctx context.Context, in *GetFolloweeRequest, opts ...grpc.CallOption) (*GetFolloweeResponse, error) {
+ out := new(GetFolloweeResponse)
+ err := c.cc.Invoke(ctx, FollowService_GetFollowee_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *followServiceClient) FollowInfo(ctx context.Context, in *FollowInfoRequest, opts ...grpc.CallOption) (*FollowInfoResponse, error) {
+ out := new(FollowInfoResponse)
+ err := c.cc.Invoke(ctx, FollowService_FollowInfo_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *followServiceClient) GetFollower(ctx context.Context, in *GetFollowerRequest, opts ...grpc.CallOption) (*GetFollowerResponse, error) {
+ out := new(GetFollowerResponse)
+ err := c.cc.Invoke(ctx, FollowService_GetFollower_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *followServiceClient) GetFollowStatic(ctx context.Context, in *GetFollowStaticRequest, opts ...grpc.CallOption) (*GetFollowStaticResponse, error) {
+ out := new(GetFollowStaticResponse)
+ err := c.cc.Invoke(ctx, FollowService_GetFollowStatic_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// FollowServiceServer is the server API for FollowService service.
+// All implementations must embed UnimplementedFollowServiceServer
+// for forward compatibility
+type FollowServiceServer interface {
+ // 增删
+ Follow(context.Context, *FollowRequest) (*FollowResponse, error)
+ CancelFollow(context.Context, *CancelFollowRequest) (*CancelFollowResponse, error)
+ // 获得某个人的关注列表
+ GetFollowee(context.Context, *GetFolloweeRequest) (*GetFolloweeResponse, error)
+ // 获得某个人关注另外一个人的详细信息
+ FollowInfo(context.Context, *FollowInfoRequest) (*FollowInfoResponse, error)
+ // 获取某人的粉丝列表
+ GetFollower(context.Context, *GetFollowerRequest) (*GetFollowerResponse, error)
+ // 获取默认的关注人数
+ GetFollowStatic(context.Context, *GetFollowStaticRequest) (*GetFollowStaticResponse, error)
+ mustEmbedUnimplementedFollowServiceServer()
+}
+
+// UnimplementedFollowServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedFollowServiceServer struct {
+}
+
+func (UnimplementedFollowServiceServer) Follow(context.Context, *FollowRequest) (*FollowResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Follow not implemented")
+}
+func (UnimplementedFollowServiceServer) CancelFollow(context.Context, *CancelFollowRequest) (*CancelFollowResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CancelFollow not implemented")
+}
+func (UnimplementedFollowServiceServer) GetFollowee(context.Context, *GetFolloweeRequest) (*GetFolloweeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetFollowee not implemented")
+}
+func (UnimplementedFollowServiceServer) FollowInfo(context.Context, *FollowInfoRequest) (*FollowInfoResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FollowInfo not implemented")
+}
+func (UnimplementedFollowServiceServer) GetFollower(context.Context, *GetFollowerRequest) (*GetFollowerResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetFollower not implemented")
+}
+func (UnimplementedFollowServiceServer) GetFollowStatic(context.Context, *GetFollowStaticRequest) (*GetFollowStaticResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetFollowStatic not implemented")
+}
+func (UnimplementedFollowServiceServer) mustEmbedUnimplementedFollowServiceServer() {}
+
+// UnsafeFollowServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to FollowServiceServer will
+// result in compilation errors.
+type UnsafeFollowServiceServer interface {
+ mustEmbedUnimplementedFollowServiceServer()
+}
+
+func RegisterFollowServiceServer(s grpc.ServiceRegistrar, srv FollowServiceServer) {
+ s.RegisterService(&FollowService_ServiceDesc, srv)
+}
+
+func _FollowService_Follow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FollowRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).Follow(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_Follow_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).Follow(ctx, req.(*FollowRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FollowService_CancelFollow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CancelFollowRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).CancelFollow(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_CancelFollow_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).CancelFollow(ctx, req.(*CancelFollowRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FollowService_GetFollowee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetFolloweeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).GetFollowee(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_GetFollowee_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).GetFollowee(ctx, req.(*GetFolloweeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FollowService_FollowInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FollowInfoRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).FollowInfo(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_FollowInfo_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).FollowInfo(ctx, req.(*FollowInfoRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FollowService_GetFollower_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetFollowerRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).GetFollower(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_GetFollower_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).GetFollower(ctx, req.(*GetFollowerRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _FollowService_GetFollowStatic_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetFollowStaticRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(FollowServiceServer).GetFollowStatic(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: FollowService_GetFollowStatic_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(FollowServiceServer).GetFollowStatic(ctx, req.(*GetFollowStaticRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// FollowService_ServiceDesc is the grpc.ServiceDesc for FollowService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var FollowService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "follow.v1.FollowService",
+ HandlerType: (*FollowServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Follow",
+ Handler: _FollowService_Follow_Handler,
+ },
+ {
+ MethodName: "CancelFollow",
+ Handler: _FollowService_CancelFollow_Handler,
+ },
+ {
+ MethodName: "GetFollowee",
+ Handler: _FollowService_GetFollowee_Handler,
+ },
+ {
+ MethodName: "FollowInfo",
+ Handler: _FollowService_FollowInfo_Handler,
+ },
+ {
+ MethodName: "GetFollower",
+ Handler: _FollowService_GetFollower_Handler,
+ },
+ {
+ MethodName: "GetFollowStatic",
+ Handler: _FollowService_GetFollowStatic_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "follow/v1/follow.proto",
+}
diff --git a/webook/api/proto/gen/follow/v1/mocks/follow_grpc.mock.go b/webook/api/proto/gen/follow/v1/mocks/follow_grpc.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..c464c6d31bd0abb91708205ba60cf21154571527
--- /dev/null
+++ b/webook/api/proto/gen/follow/v1/mocks/follow_grpc.mock.go
@@ -0,0 +1,321 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/api/proto/gen/follow/v1/follow_grpc.pb.go
+//
+// Generated by this command:
+//
+// mockgen -source=webook/api/proto/gen/follow/v1/follow_grpc.pb.go -package=followmocks -destination=webook/api/proto/gen/follow/v1/mocks/follow_grpc.mock.go
+//
+// Package followmocks is a generated GoMock package.
+package followmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ gomock "go.uber.org/mock/gomock"
+ grpc "google.golang.org/grpc"
+)
+
+// MockFollowServiceClient is a mock of FollowServiceClient interface.
+type MockFollowServiceClient struct {
+ ctrl *gomock.Controller
+ recorder *MockFollowServiceClientMockRecorder
+}
+
+// MockFollowServiceClientMockRecorder is the mock recorder for MockFollowServiceClient.
+type MockFollowServiceClientMockRecorder struct {
+ mock *MockFollowServiceClient
+}
+
+// NewMockFollowServiceClient creates a new mock instance.
+func NewMockFollowServiceClient(ctrl *gomock.Controller) *MockFollowServiceClient {
+ mock := &MockFollowServiceClient{ctrl: ctrl}
+ mock.recorder = &MockFollowServiceClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockFollowServiceClient) EXPECT() *MockFollowServiceClientMockRecorder {
+ return m.recorder
+}
+
+// CancelFollow mocks base method.
+func (m *MockFollowServiceClient) CancelFollow(ctx context.Context, in *followv1.CancelFollowRequest, opts ...grpc.CallOption) (*followv1.CancelFollowResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "CancelFollow", varargs...)
+ ret0, _ := ret[0].(*followv1.CancelFollowResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CancelFollow indicates an expected call of CancelFollow.
+func (mr *MockFollowServiceClientMockRecorder) CancelFollow(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelFollow", reflect.TypeOf((*MockFollowServiceClient)(nil).CancelFollow), varargs...)
+}
+
+// Follow mocks base method.
+func (m *MockFollowServiceClient) Follow(ctx context.Context, in *followv1.FollowRequest, opts ...grpc.CallOption) (*followv1.FollowResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Follow", varargs...)
+ ret0, _ := ret[0].(*followv1.FollowResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Follow indicates an expected call of Follow.
+func (mr *MockFollowServiceClientMockRecorder) Follow(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Follow", reflect.TypeOf((*MockFollowServiceClient)(nil).Follow), varargs...)
+}
+
+// FollowInfo mocks base method.
+func (m *MockFollowServiceClient) FollowInfo(ctx context.Context, in *followv1.FollowInfoRequest, opts ...grpc.CallOption) (*followv1.FollowInfoResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "FollowInfo", varargs...)
+ ret0, _ := ret[0].(*followv1.FollowInfoResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FollowInfo indicates an expected call of FollowInfo.
+func (mr *MockFollowServiceClientMockRecorder) FollowInfo(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FollowInfo", reflect.TypeOf((*MockFollowServiceClient)(nil).FollowInfo), varargs...)
+}
+
+// GetFollowStatic mocks base method.
+func (m *MockFollowServiceClient) GetFollowStatic(ctx context.Context, in *followv1.GetFollowStaticRequest, opts ...grpc.CallOption) (*followv1.GetFollowStaticResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetFollowStatic", varargs...)
+ ret0, _ := ret[0].(*followv1.GetFollowStaticResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollowStatic indicates an expected call of GetFollowStatic.
+func (mr *MockFollowServiceClientMockRecorder) GetFollowStatic(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowStatic", reflect.TypeOf((*MockFollowServiceClient)(nil).GetFollowStatic), varargs...)
+}
+
+// GetFollowee mocks base method.
+func (m *MockFollowServiceClient) GetFollowee(ctx context.Context, in *followv1.GetFolloweeRequest, opts ...grpc.CallOption) (*followv1.GetFolloweeResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetFollowee", varargs...)
+ ret0, _ := ret[0].(*followv1.GetFolloweeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollowee indicates an expected call of GetFollowee.
+func (mr *MockFollowServiceClientMockRecorder) GetFollowee(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowee", reflect.TypeOf((*MockFollowServiceClient)(nil).GetFollowee), varargs...)
+}
+
+// GetFollower mocks base method.
+func (m *MockFollowServiceClient) GetFollower(ctx context.Context, in *followv1.GetFollowerRequest, opts ...grpc.CallOption) (*followv1.GetFollowerResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetFollower", varargs...)
+ ret0, _ := ret[0].(*followv1.GetFollowerResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollower indicates an expected call of GetFollower.
+func (mr *MockFollowServiceClientMockRecorder) GetFollower(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollower", reflect.TypeOf((*MockFollowServiceClient)(nil).GetFollower), varargs...)
+}
+
+// MockFollowServiceServer is a mock of FollowServiceServer interface.
+type MockFollowServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockFollowServiceServerMockRecorder
+}
+
+// MockFollowServiceServerMockRecorder is the mock recorder for MockFollowServiceServer.
+type MockFollowServiceServerMockRecorder struct {
+ mock *MockFollowServiceServer
+}
+
+// NewMockFollowServiceServer creates a new mock instance.
+func NewMockFollowServiceServer(ctrl *gomock.Controller) *MockFollowServiceServer {
+ mock := &MockFollowServiceServer{ctrl: ctrl}
+ mock.recorder = &MockFollowServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockFollowServiceServer) EXPECT() *MockFollowServiceServerMockRecorder {
+ return m.recorder
+}
+
+// CancelFollow mocks base method.
+func (m *MockFollowServiceServer) CancelFollow(arg0 context.Context, arg1 *followv1.CancelFollowRequest) (*followv1.CancelFollowResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CancelFollow", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.CancelFollowResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CancelFollow indicates an expected call of CancelFollow.
+func (mr *MockFollowServiceServerMockRecorder) CancelFollow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelFollow", reflect.TypeOf((*MockFollowServiceServer)(nil).CancelFollow), arg0, arg1)
+}
+
+// Follow mocks base method.
+func (m *MockFollowServiceServer) Follow(arg0 context.Context, arg1 *followv1.FollowRequest) (*followv1.FollowResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Follow", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.FollowResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Follow indicates an expected call of Follow.
+func (mr *MockFollowServiceServerMockRecorder) Follow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Follow", reflect.TypeOf((*MockFollowServiceServer)(nil).Follow), arg0, arg1)
+}
+
+// FollowInfo mocks base method.
+func (m *MockFollowServiceServer) FollowInfo(arg0 context.Context, arg1 *followv1.FollowInfoRequest) (*followv1.FollowInfoResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FollowInfo", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.FollowInfoResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FollowInfo indicates an expected call of FollowInfo.
+func (mr *MockFollowServiceServerMockRecorder) FollowInfo(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FollowInfo", reflect.TypeOf((*MockFollowServiceServer)(nil).FollowInfo), arg0, arg1)
+}
+
+// GetFollowStatic mocks base method.
+func (m *MockFollowServiceServer) GetFollowStatic(arg0 context.Context, arg1 *followv1.GetFollowStaticRequest) (*followv1.GetFollowStaticResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFollowStatic", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.GetFollowStaticResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollowStatic indicates an expected call of GetFollowStatic.
+func (mr *MockFollowServiceServerMockRecorder) GetFollowStatic(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowStatic", reflect.TypeOf((*MockFollowServiceServer)(nil).GetFollowStatic), arg0, arg1)
+}
+
+// GetFollowee mocks base method.
+func (m *MockFollowServiceServer) GetFollowee(arg0 context.Context, arg1 *followv1.GetFolloweeRequest) (*followv1.GetFolloweeResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFollowee", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.GetFolloweeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollowee indicates an expected call of GetFollowee.
+func (mr *MockFollowServiceServerMockRecorder) GetFollowee(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowee", reflect.TypeOf((*MockFollowServiceServer)(nil).GetFollowee), arg0, arg1)
+}
+
+// GetFollower mocks base method.
+func (m *MockFollowServiceServer) GetFollower(arg0 context.Context, arg1 *followv1.GetFollowerRequest) (*followv1.GetFollowerResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFollower", arg0, arg1)
+ ret0, _ := ret[0].(*followv1.GetFollowerResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetFollower indicates an expected call of GetFollower.
+func (mr *MockFollowServiceServerMockRecorder) GetFollower(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollower", reflect.TypeOf((*MockFollowServiceServer)(nil).GetFollower), arg0, arg1)
+}
+
+// mustEmbedUnimplementedFollowServiceServer mocks base method.
+func (m *MockFollowServiceServer) mustEmbedUnimplementedFollowServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedFollowServiceServer")
+}
+
+// mustEmbedUnimplementedFollowServiceServer indicates an expected call of mustEmbedUnimplementedFollowServiceServer.
+func (mr *MockFollowServiceServerMockRecorder) mustEmbedUnimplementedFollowServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedFollowServiceServer", reflect.TypeOf((*MockFollowServiceServer)(nil).mustEmbedUnimplementedFollowServiceServer))
+}
+
+// MockUnsafeFollowServiceServer is a mock of UnsafeFollowServiceServer interface.
+type MockUnsafeFollowServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockUnsafeFollowServiceServerMockRecorder
+}
+
+// MockUnsafeFollowServiceServerMockRecorder is the mock recorder for MockUnsafeFollowServiceServer.
+type MockUnsafeFollowServiceServerMockRecorder struct {
+ mock *MockUnsafeFollowServiceServer
+}
+
+// NewMockUnsafeFollowServiceServer creates a new mock instance.
+func NewMockUnsafeFollowServiceServer(ctrl *gomock.Controller) *MockUnsafeFollowServiceServer {
+ mock := &MockUnsafeFollowServiceServer{ctrl: ctrl}
+ mock.recorder = &MockUnsafeFollowServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUnsafeFollowServiceServer) EXPECT() *MockUnsafeFollowServiceServerMockRecorder {
+ return m.recorder
+}
+
+// mustEmbedUnimplementedFollowServiceServer mocks base method.
+func (m *MockUnsafeFollowServiceServer) mustEmbedUnimplementedFollowServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedFollowServiceServer")
+}
+
+// mustEmbedUnimplementedFollowServiceServer indicates an expected call of mustEmbedUnimplementedFollowServiceServer.
+func (mr *MockUnsafeFollowServiceServerMockRecorder) mustEmbedUnimplementedFollowServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedFollowServiceServer", reflect.TypeOf((*MockUnsafeFollowServiceServer)(nil).mustEmbedUnimplementedFollowServiceServer))
+}
diff --git a/webook/api/proto/gen/intr/v1/interactive.pb.go b/webook/api/proto/gen/intr/v1/interactive.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..984c629703d90e65c36d191c8e2893c6d36509d8
--- /dev/null
+++ b/webook/api/proto/gen/intr/v1/interactive.pb.go
@@ -0,0 +1,1068 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: intr/v1/interactive.proto
+
+package intrv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GetByIdsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ Ids []int64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
+}
+
+func (x *GetByIdsRequest) Reset() {
+ *x = GetByIdsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdsRequest) ProtoMessage() {}
+
+func (x *GetByIdsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdsRequest.ProtoReflect.Descriptor instead.
+func (*GetByIdsRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetByIdsRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *GetByIdsRequest) GetIds() []int64 {
+ if x != nil {
+ return x.Ids
+ }
+ return nil
+}
+
+type GetByIdsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Intrs map[int64]*Interactive `protobuf:"bytes,1,rep,name=intrs,proto3" json:"intrs,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (x *GetByIdsResponse) Reset() {
+ *x = GetByIdsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdsResponse) ProtoMessage() {}
+
+func (x *GetByIdsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdsResponse.ProtoReflect.Descriptor instead.
+func (*GetByIdsResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetByIdsResponse) GetIntrs() map[int64]*Interactive {
+ if x != nil {
+ return x.Intrs
+ }
+ return nil
+}
+
+type Interactive struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ ReadCnt int64 `protobuf:"varint,3,opt,name=read_cnt,json=readCnt,proto3" json:"read_cnt,omitempty"`
+ LikeCnt int64 `protobuf:"varint,4,opt,name=like_cnt,json=likeCnt,proto3" json:"like_cnt,omitempty"`
+ CollectCnt int64 `protobuf:"varint,5,opt,name=collect_cnt,json=collectCnt,proto3" json:"collect_cnt,omitempty"`
+ Liked bool `protobuf:"varint,6,opt,name=liked,proto3" json:"liked,omitempty"`
+ Collected bool `protobuf:"varint,7,opt,name=collected,proto3" json:"collected,omitempty"`
+}
+
+func (x *Interactive) Reset() {
+ *x = Interactive{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Interactive) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Interactive) ProtoMessage() {}
+
+func (x *Interactive) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Interactive.ProtoReflect.Descriptor instead.
+func (*Interactive) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *Interactive) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *Interactive) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *Interactive) GetReadCnt() int64 {
+ if x != nil {
+ return x.ReadCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetLikeCnt() int64 {
+ if x != nil {
+ return x.LikeCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetCollectCnt() int64 {
+ if x != nil {
+ return x.CollectCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetLiked() bool {
+ if x != nil {
+ return x.Liked
+ }
+ return false
+}
+
+func (x *Interactive) GetCollected() bool {
+ if x != nil {
+ return x.Collected
+ }
+ return false
+}
+
+type GetResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Intr *Interactive `protobuf:"bytes,1,opt,name=intr,proto3" json:"intr,omitempty"`
+}
+
+func (x *GetResponse) Reset() {
+ *x = GetResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetResponse) ProtoMessage() {}
+
+func (x *GetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetResponse.ProtoReflect.Descriptor instead.
+func (*GetResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GetResponse) GetIntr() *Interactive {
+ if x != nil {
+ return x.Intr
+ }
+ return nil
+}
+
+type GetRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *GetRequest) Reset() {
+ *x = GetRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetRequest) ProtoMessage() {}
+
+func (x *GetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead.
+func (*GetRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *GetRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *GetRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type CollectResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CollectResponse) Reset() {
+ *x = CollectResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CollectResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CollectResponse) ProtoMessage() {}
+
+func (x *CollectResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CollectResponse.ProtoReflect.Descriptor instead.
+func (*CollectResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{5}
+}
+
+type CollectRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+ Cid int64 `protobuf:"varint,4,opt,name=cid,proto3" json:"cid,omitempty"`
+}
+
+func (x *CollectRequest) Reset() {
+ *x = CollectRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CollectRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CollectRequest) ProtoMessage() {}
+
+func (x *CollectRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CollectRequest.ProtoReflect.Descriptor instead.
+func (*CollectRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *CollectRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CollectRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *CollectRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *CollectRequest) GetCid() int64 {
+ if x != nil {
+ return x.Cid
+ }
+ return 0
+}
+
+type CancelLikeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CancelLikeResponse) Reset() {
+ *x = CancelLikeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelLikeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelLikeResponse) ProtoMessage() {}
+
+func (x *CancelLikeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelLikeResponse.ProtoReflect.Descriptor instead.
+func (*CancelLikeResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{7}
+}
+
+type CancelLikeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *CancelLikeRequest) Reset() {
+ *x = CancelLikeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelLikeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelLikeRequest) ProtoMessage() {}
+
+func (x *CancelLikeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelLikeRequest.ProtoReflect.Descriptor instead.
+func (*CancelLikeRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *CancelLikeRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CancelLikeRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *CancelLikeRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type LikeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *LikeRequest) Reset() {
+ *x = LikeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LikeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LikeRequest) ProtoMessage() {}
+
+func (x *LikeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LikeRequest.ProtoReflect.Descriptor instead.
+func (*LikeRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *LikeRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *LikeRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *LikeRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type LikeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *LikeResponse) Reset() {
+ *x = LikeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LikeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LikeResponse) ProtoMessage() {}
+
+func (x *LikeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LikeResponse.ProtoReflect.Descriptor instead.
+func (*LikeResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{10}
+}
+
+type IncrReadCntRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+}
+
+func (x *IncrReadCntRequest) Reset() {
+ *x = IncrReadCntRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *IncrReadCntRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IncrReadCntRequest) ProtoMessage() {}
+
+func (x *IncrReadCntRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[11]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use IncrReadCntRequest.ProtoReflect.Descriptor instead.
+func (*IncrReadCntRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *IncrReadCntRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *IncrReadCntRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+type IncrReadCntResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *IncrReadCntResponse) Reset() {
+ *x = IncrReadCntResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_interactive_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *IncrReadCntResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IncrReadCntResponse) ProtoMessage() {}
+
+func (x *IncrReadCntResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_interactive_proto_msgTypes[12]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use IncrReadCntResponse.ProtoReflect.Descriptor instead.
+func (*IncrReadCntResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_interactive_proto_rawDescGZIP(), []int{12}
+}
+
+var File_intr_v1_interactive_proto protoreflect.FileDescriptor
+
+var file_intr_v1_interactive_proto_rawDesc = []byte{
+ 0x0a, 0x19, 0x69, 0x6e, 0x74, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x61,
+ 0x63, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x69, 0x6e, 0x74,
+ 0x72, 0x2e, 0x76, 0x31, 0x22, 0x35, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73,
+ 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x9e, 0x01, 0x0a, 0x10,
+ 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x3a, 0x0a, 0x05, 0x69, 0x6e, 0x74, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x24, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49,
+ 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x6e, 0x74, 0x72, 0x73,
+ 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x74, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x0a,
+ 0x49, 0x6e, 0x74, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+ 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e,
+ 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76,
+ 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc1, 0x01, 0x0a,
+ 0x0b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03,
+ 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15,
+ 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x63, 0x6e,
+ 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74,
+ 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x69, 0x6b, 0x65, 0x5f, 0x63, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x07, 0x6c, 0x69, 0x6b, 0x65, 0x43, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63,
+ 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x43, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05,
+ 0x6c, 0x69, 0x6b, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6c, 0x69, 0x6b,
+ 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18,
+ 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64,
+ 0x22, 0x37, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x28, 0x0a, 0x04, 0x69, 0x6e, 0x74, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e,
+ 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74,
+ 0x69, 0x76, 0x65, 0x52, 0x04, 0x69, 0x6e, 0x74, 0x72, 0x22, 0x47, 0x0a, 0x0a, 0x47, 0x65, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64,
+ 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75,
+ 0x69, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5d, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64,
+ 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75,
+ 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x03, 0x63, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69,
+ 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x0a, 0x11, 0x43, 0x61,
+ 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69,
+ 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x48, 0x0a, 0x0b, 0x4c, 0x69,
+ 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62,
+ 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a,
+ 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x03, 0x75, 0x69, 0x64, 0x22, 0x0e, 0x0a, 0x0c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64,
+ 0x43, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69,
+ 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06,
+ 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69,
+ 0x7a, 0x49, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43,
+ 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x03, 0x0a, 0x12, 0x49,
+ 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x48, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74,
+ 0x12, 0x1b, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x63, 0x72, 0x52,
+ 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e,
+ 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64,
+ 0x43, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x4c,
+ 0x69, 0x6b, 0x65, 0x12, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69,
+ 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x69, 0x6e, 0x74, 0x72,
+ 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x12, 0x1a,
+ 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c,
+ 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6e, 0x74,
+ 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x6c, 0x65,
+ 0x63, 0x74, 0x12, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c,
+ 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x69, 0x6e,
+ 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x13, 0x2e, 0x69,
+ 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, 0x79,
+ 0x49, 0x64, 0x73, 0x12, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
+ 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e,
+ 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d,
+ 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x10, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61,
+ 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f, 0x67, 0x69,
+ 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67,
+ 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b,
+ 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x69,
+ 0x6e, 0x74, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6e, 0x74, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03,
+ 0x49, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07,
+ 0x49, 0x6e, 0x74, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x49, 0x6e, 0x74, 0x72, 0x5c, 0x56,
+ 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08,
+ 0x49, 0x6e, 0x74, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_intr_v1_interactive_proto_rawDescOnce sync.Once
+ file_intr_v1_interactive_proto_rawDescData = file_intr_v1_interactive_proto_rawDesc
+)
+
+func file_intr_v1_interactive_proto_rawDescGZIP() []byte {
+ file_intr_v1_interactive_proto_rawDescOnce.Do(func() {
+ file_intr_v1_interactive_proto_rawDescData = protoimpl.X.CompressGZIP(file_intr_v1_interactive_proto_rawDescData)
+ })
+ return file_intr_v1_interactive_proto_rawDescData
+}
+
+var file_intr_v1_interactive_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_intr_v1_interactive_proto_goTypes = []interface{}{
+ (*GetByIdsRequest)(nil), // 0: intr.v1.GetByIdsRequest
+ (*GetByIdsResponse)(nil), // 1: intr.v1.GetByIdsResponse
+ (*Interactive)(nil), // 2: intr.v1.Interactive
+ (*GetResponse)(nil), // 3: intr.v1.GetResponse
+ (*GetRequest)(nil), // 4: intr.v1.GetRequest
+ (*CollectResponse)(nil), // 5: intr.v1.CollectResponse
+ (*CollectRequest)(nil), // 6: intr.v1.CollectRequest
+ (*CancelLikeResponse)(nil), // 7: intr.v1.CancelLikeResponse
+ (*CancelLikeRequest)(nil), // 8: intr.v1.CancelLikeRequest
+ (*LikeRequest)(nil), // 9: intr.v1.LikeRequest
+ (*LikeResponse)(nil), // 10: intr.v1.LikeResponse
+ (*IncrReadCntRequest)(nil), // 11: intr.v1.IncrReadCntRequest
+ (*IncrReadCntResponse)(nil), // 12: intr.v1.IncrReadCntResponse
+ nil, // 13: intr.v1.GetByIdsResponse.IntrsEntry
+}
+var file_intr_v1_interactive_proto_depIdxs = []int32{
+ 13, // 0: intr.v1.GetByIdsResponse.intrs:type_name -> intr.v1.GetByIdsResponse.IntrsEntry
+ 2, // 1: intr.v1.GetResponse.intr:type_name -> intr.v1.Interactive
+ 2, // 2: intr.v1.GetByIdsResponse.IntrsEntry.value:type_name -> intr.v1.Interactive
+ 11, // 3: intr.v1.InteractiveService.IncrReadCnt:input_type -> intr.v1.IncrReadCntRequest
+ 9, // 4: intr.v1.InteractiveService.Like:input_type -> intr.v1.LikeRequest
+ 8, // 5: intr.v1.InteractiveService.CancelLike:input_type -> intr.v1.CancelLikeRequest
+ 6, // 6: intr.v1.InteractiveService.Collect:input_type -> intr.v1.CollectRequest
+ 4, // 7: intr.v1.InteractiveService.Get:input_type -> intr.v1.GetRequest
+ 0, // 8: intr.v1.InteractiveService.GetByIds:input_type -> intr.v1.GetByIdsRequest
+ 12, // 9: intr.v1.InteractiveService.IncrReadCnt:output_type -> intr.v1.IncrReadCntResponse
+ 10, // 10: intr.v1.InteractiveService.Like:output_type -> intr.v1.LikeResponse
+ 7, // 11: intr.v1.InteractiveService.CancelLike:output_type -> intr.v1.CancelLikeResponse
+ 5, // 12: intr.v1.InteractiveService.Collect:output_type -> intr.v1.CollectResponse
+ 3, // 13: intr.v1.InteractiveService.Get:output_type -> intr.v1.GetResponse
+ 1, // 14: intr.v1.InteractiveService.GetByIds:output_type -> intr.v1.GetByIdsResponse
+ 9, // [9:15] is the sub-list for method output_type
+ 3, // [3:9] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_intr_v1_interactive_proto_init() }
+func file_intr_v1_interactive_proto_init() {
+ if File_intr_v1_interactive_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_intr_v1_interactive_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Interactive); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CollectResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CollectRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelLikeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelLikeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LikeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LikeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*IncrReadCntRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_interactive_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*IncrReadCntResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_intr_v1_interactive_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 14,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_intr_v1_interactive_proto_goTypes,
+ DependencyIndexes: file_intr_v1_interactive_proto_depIdxs,
+ MessageInfos: file_intr_v1_interactive_proto_msgTypes,
+ }.Build()
+ File_intr_v1_interactive_proto = out.File
+ file_intr_v1_interactive_proto_rawDesc = nil
+ file_intr_v1_interactive_proto_goTypes = nil
+ file_intr_v1_interactive_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/intr/v1/interactive_grpc.pb.go b/webook/api/proto/gen/intr/v1/interactive_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..52171ef4895a2b2a004a519577c26340d4038d57
--- /dev/null
+++ b/webook/api/proto/gen/intr/v1/interactive_grpc.pb.go
@@ -0,0 +1,298 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: intr/v1/interactive.proto
+
+package intrv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ InteractiveService_IncrReadCnt_FullMethodName = "/intr.v1.InteractiveService/IncrReadCnt"
+ InteractiveService_Like_FullMethodName = "/intr.v1.InteractiveService/Like"
+ InteractiveService_CancelLike_FullMethodName = "/intr.v1.InteractiveService/CancelLike"
+ InteractiveService_Collect_FullMethodName = "/intr.v1.InteractiveService/Collect"
+ InteractiveService_Get_FullMethodName = "/intr.v1.InteractiveService/Get"
+ InteractiveService_GetByIds_FullMethodName = "/intr.v1.InteractiveService/GetByIds"
+)
+
+// InteractiveServiceClient is the client API for InteractiveService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type InteractiveServiceClient interface {
+ IncrReadCnt(ctx context.Context, in *IncrReadCntRequest, opts ...grpc.CallOption) (*IncrReadCntResponse, error)
+ Like(ctx context.Context, in *LikeRequest, opts ...grpc.CallOption) (*LikeResponse, error)
+ // CancelLike 取消点赞
+ CancelLike(ctx context.Context, in *CancelLikeRequest, opts ...grpc.CallOption) (*CancelLikeResponse, error)
+ // Collect 收藏
+ Collect(ctx context.Context, in *CollectRequest, opts ...grpc.CallOption) (*CollectResponse, error)
+ Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
+ GetByIds(ctx context.Context, in *GetByIdsRequest, opts ...grpc.CallOption) (*GetByIdsResponse, error)
+}
+
+type interactiveServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewInteractiveServiceClient(cc grpc.ClientConnInterface) InteractiveServiceClient {
+ return &interactiveServiceClient{cc}
+}
+
+func (c *interactiveServiceClient) IncrReadCnt(ctx context.Context, in *IncrReadCntRequest, opts ...grpc.CallOption) (*IncrReadCntResponse, error) {
+ out := new(IncrReadCntResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_IncrReadCnt_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Like(ctx context.Context, in *LikeRequest, opts ...grpc.CallOption) (*LikeResponse, error) {
+ out := new(LikeResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Like_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) CancelLike(ctx context.Context, in *CancelLikeRequest, opts ...grpc.CallOption) (*CancelLikeResponse, error) {
+ out := new(CancelLikeResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_CancelLike_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Collect(ctx context.Context, in *CollectRequest, opts ...grpc.CallOption) (*CollectResponse, error) {
+ out := new(CollectResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Collect_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
+ out := new(GetResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Get_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) GetByIds(ctx context.Context, in *GetByIdsRequest, opts ...grpc.CallOption) (*GetByIdsResponse, error) {
+ out := new(GetByIdsResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_GetByIds_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// InteractiveServiceServer is the server API for InteractiveService service.
+// All implementations must embed UnimplementedInteractiveServiceServer
+// for forward compatibility
+type InteractiveServiceServer interface {
+ IncrReadCnt(context.Context, *IncrReadCntRequest) (*IncrReadCntResponse, error)
+ Like(context.Context, *LikeRequest) (*LikeResponse, error)
+ // CancelLike 取消点赞
+ CancelLike(context.Context, *CancelLikeRequest) (*CancelLikeResponse, error)
+ // Collect 收藏
+ Collect(context.Context, *CollectRequest) (*CollectResponse, error)
+ Get(context.Context, *GetRequest) (*GetResponse, error)
+ GetByIds(context.Context, *GetByIdsRequest) (*GetByIdsResponse, error)
+ mustEmbedUnimplementedInteractiveServiceServer()
+}
+
+// UnimplementedInteractiveServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedInteractiveServiceServer struct {
+}
+
+func (UnimplementedInteractiveServiceServer) IncrReadCnt(context.Context, *IncrReadCntRequest) (*IncrReadCntResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method IncrReadCnt not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Like(context.Context, *LikeRequest) (*LikeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Like not implemented")
+}
+func (UnimplementedInteractiveServiceServer) CancelLike(context.Context, *CancelLikeRequest) (*CancelLikeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CancelLike not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Collect(context.Context, *CollectRequest) (*CollectResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Collect not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Get(context.Context, *GetRequest) (*GetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
+}
+func (UnimplementedInteractiveServiceServer) GetByIds(context.Context, *GetByIdsRequest) (*GetByIdsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetByIds not implemented")
+}
+func (UnimplementedInteractiveServiceServer) mustEmbedUnimplementedInteractiveServiceServer() {}
+
+// UnsafeInteractiveServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to InteractiveServiceServer will
+// result in compilation errors.
+type UnsafeInteractiveServiceServer interface {
+ mustEmbedUnimplementedInteractiveServiceServer()
+}
+
+func RegisterInteractiveServiceServer(s grpc.ServiceRegistrar, srv InteractiveServiceServer) {
+ s.RegisterService(&InteractiveService_ServiceDesc, srv)
+}
+
+func _InteractiveService_IncrReadCnt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(IncrReadCntRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).IncrReadCnt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_IncrReadCnt_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).IncrReadCnt(ctx, req.(*IncrReadCntRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Like_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(LikeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Like(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Like_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Like(ctx, req.(*LikeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_CancelLike_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CancelLikeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).CancelLike(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_CancelLike_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).CancelLike(ctx, req.(*CancelLikeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Collect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CollectRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Collect(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Collect_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Collect(ctx, req.(*CollectRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Get(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Get_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Get(ctx, req.(*GetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_GetByIds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetByIdsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).GetByIds(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_GetByIds_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).GetByIds(ctx, req.(*GetByIdsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// InteractiveService_ServiceDesc is the grpc.ServiceDesc for InteractiveService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var InteractiveService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "intr.v1.InteractiveService",
+ HandlerType: (*InteractiveServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "IncrReadCnt",
+ Handler: _InteractiveService_IncrReadCnt_Handler,
+ },
+ {
+ MethodName: "Like",
+ Handler: _InteractiveService_Like_Handler,
+ },
+ {
+ MethodName: "CancelLike",
+ Handler: _InteractiveService_CancelLike_Handler,
+ },
+ {
+ MethodName: "Collect",
+ Handler: _InteractiveService_Collect_Handler,
+ },
+ {
+ MethodName: "Get",
+ Handler: _InteractiveService_Get_Handler,
+ },
+ {
+ MethodName: "GetByIds",
+ Handler: _InteractiveService_GetByIds_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "intr/v1/interactive.proto",
+}
diff --git a/webook/api/proto/gen/intr/v1/intr.pb.go b/webook/api/proto/gen/intr/v1/intr.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..38ac1e100e3c58eac2f74c9c66da6bd842b00032
--- /dev/null
+++ b/webook/api/proto/gen/intr/v1/intr.pb.go
@@ -0,0 +1,1073 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: intr/v1/intr.proto
+
+package intrv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GetByIdsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ Ids []int64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
+}
+
+func (x *GetByIdsRequest) Reset() {
+ *x = GetByIdsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdsRequest) ProtoMessage() {}
+
+func (x *GetByIdsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdsRequest.ProtoReflect.Descriptor instead.
+func (*GetByIdsRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetByIdsRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *GetByIdsRequest) GetIds() []int64 {
+ if x != nil {
+ return x.Ids
+ }
+ return nil
+}
+
+type GetByIdsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Intrs map[int64]*Interactive `protobuf:"bytes,1,rep,name=intrs,proto3" json:"intrs,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (x *GetByIdsResponse) Reset() {
+ *x = GetByIdsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetByIdsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetByIdsResponse) ProtoMessage() {}
+
+func (x *GetByIdsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetByIdsResponse.ProtoReflect.Descriptor instead.
+func (*GetByIdsResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetByIdsResponse) GetIntrs() map[int64]*Interactive {
+ if x != nil {
+ return x.Intrs
+ }
+ return nil
+}
+
+type GetRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *GetRequest) Reset() {
+ *x = GetRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetRequest) ProtoMessage() {}
+
+func (x *GetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead.
+func (*GetRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *GetRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *GetRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *GetRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type GetResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Intr *Interactive `protobuf:"bytes,1,opt,name=intr,proto3" json:"intr,omitempty"`
+}
+
+func (x *GetResponse) Reset() {
+ *x = GetResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetResponse) ProtoMessage() {}
+
+func (x *GetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetResponse.ProtoReflect.Descriptor instead.
+func (*GetResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GetResponse) GetIntr() *Interactive {
+ if x != nil {
+ return x.Intr
+ }
+ return nil
+}
+
+type Interactive struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ ReadCnt int64 `protobuf:"varint,3,opt,name=read_cnt,json=readCnt,proto3" json:"read_cnt,omitempty"`
+ LikeCnt int64 `protobuf:"varint,4,opt,name=like_cnt,json=likeCnt,proto3" json:"like_cnt,omitempty"`
+ CollectCnt int64 `protobuf:"varint,5,opt,name=collect_cnt,json=collectCnt,proto3" json:"collect_cnt,omitempty"`
+ Liked bool `protobuf:"varint,6,opt,name=liked,proto3" json:"liked,omitempty"`
+ Collected bool `protobuf:"varint,7,opt,name=collected,proto3" json:"collected,omitempty"`
+}
+
+func (x *Interactive) Reset() {
+ *x = Interactive{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Interactive) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Interactive) ProtoMessage() {}
+
+func (x *Interactive) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Interactive.ProtoReflect.Descriptor instead.
+func (*Interactive) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *Interactive) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *Interactive) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *Interactive) GetReadCnt() int64 {
+ if x != nil {
+ return x.ReadCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetLikeCnt() int64 {
+ if x != nil {
+ return x.LikeCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetCollectCnt() int64 {
+ if x != nil {
+ return x.CollectCnt
+ }
+ return 0
+}
+
+func (x *Interactive) GetLiked() bool {
+ if x != nil {
+ return x.Liked
+ }
+ return false
+}
+
+func (x *Interactive) GetCollected() bool {
+ if x != nil {
+ return x.Collected
+ }
+ return false
+}
+
+type CollectRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+ Cid int64 `protobuf:"varint,4,opt,name=cid,proto3" json:"cid,omitempty"`
+}
+
+func (x *CollectRequest) Reset() {
+ *x = CollectRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CollectRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CollectRequest) ProtoMessage() {}
+
+func (x *CollectRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CollectRequest.ProtoReflect.Descriptor instead.
+func (*CollectRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *CollectRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CollectRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *CollectRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *CollectRequest) GetCid() int64 {
+ if x != nil {
+ return x.Cid
+ }
+ return 0
+}
+
+type CollectResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CollectResponse) Reset() {
+ *x = CollectResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CollectResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CollectResponse) ProtoMessage() {}
+
+func (x *CollectResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CollectResponse.ProtoReflect.Descriptor instead.
+func (*CollectResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{6}
+}
+
+type CancelLikeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *CancelLikeRequest) Reset() {
+ *x = CancelLikeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelLikeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelLikeRequest) ProtoMessage() {}
+
+func (x *CancelLikeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelLikeRequest.ProtoReflect.Descriptor instead.
+func (*CancelLikeRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *CancelLikeRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *CancelLikeRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *CancelLikeRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type CancelLikeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *CancelLikeResponse) Reset() {
+ *x = CancelLikeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CancelLikeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelLikeResponse) ProtoMessage() {}
+
+func (x *CancelLikeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelLikeResponse.ProtoReflect.Descriptor instead.
+func (*CancelLikeResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{8}
+}
+
+type LikeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *LikeRequest) Reset() {
+ *x = LikeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LikeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LikeRequest) ProtoMessage() {}
+
+func (x *LikeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LikeRequest.ProtoReflect.Descriptor instead.
+func (*LikeRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *LikeRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *LikeRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *LikeRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type LikeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *LikeResponse) Reset() {
+ *x = LikeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LikeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LikeResponse) ProtoMessage() {}
+
+func (x *LikeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LikeResponse.ProtoReflect.Descriptor instead.
+func (*LikeResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{10}
+}
+
+type IncrReadCntRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+}
+
+func (x *IncrReadCntRequest) Reset() {
+ *x = IncrReadCntRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *IncrReadCntRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IncrReadCntRequest) ProtoMessage() {}
+
+func (x *IncrReadCntRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[11]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use IncrReadCntRequest.ProtoReflect.Descriptor instead.
+func (*IncrReadCntRequest) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *IncrReadCntRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *IncrReadCntRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+type IncrReadCntResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *IncrReadCntResponse) Reset() {
+ *x = IncrReadCntResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_intr_v1_intr_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *IncrReadCntResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IncrReadCntResponse) ProtoMessage() {}
+
+func (x *IncrReadCntResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_intr_v1_intr_proto_msgTypes[12]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use IncrReadCntResponse.ProtoReflect.Descriptor instead.
+func (*IncrReadCntResponse) Descriptor() ([]byte, []int) {
+ return file_intr_v1_intr_proto_rawDescGZIP(), []int{12}
+}
+
+var File_intr_v1_intr_proto protoreflect.FileDescriptor
+
+var file_intr_v1_intr_proto_rawDesc = []byte{
+ 0x0a, 0x12, 0x69, 0x6e, 0x74, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x35, 0x0a,
+ 0x0f, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62,
+ 0x69, 0x7a, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52,
+ 0x03, 0x69, 0x64, 0x73, 0x22, 0x9e, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64,
+ 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x05, 0x69, 0x6e, 0x74,
+ 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x6e, 0x74, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05,
+ 0x69, 0x6e, 0x74, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x0a, 0x49, 0x6e, 0x74, 0x72, 0x73, 0x45, 0x6e,
+ 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49,
+ 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03,
+ 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x37,
+ 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a,
+ 0x04, 0x69, 0x6e, 0x74, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e,
+ 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76,
+ 0x65, 0x52, 0x04, 0x69, 0x6e, 0x74, 0x72, 0x22, 0xc1, 0x01, 0x0a, 0x0b, 0x49, 0x6e, 0x74, 0x65,
+ 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64,
+ 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x63, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x07, 0x72, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6c,
+ 0x69, 0x6b, 0x65, 0x5f, 0x63, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6c,
+ 0x69, 0x6b, 0x65, 0x43, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63,
+ 0x74, 0x5f, 0x63, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x63, 0x6f, 0x6c,
+ 0x6c, 0x65, 0x63, 0x74, 0x43, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6b, 0x65, 0x64,
+ 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6c, 0x69, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x0a,
+ 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08,
+ 0x52, 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x5d, 0x0a, 0x0e, 0x43,
+ 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
+ 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12,
+ 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x69, 0x64, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x63, 0x69, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x43, 0x6f,
+ 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x0a,
+ 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75,
+ 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x14, 0x0a,
+ 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x22, 0x48, 0x0a, 0x0b, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75,
+ 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x0e, 0x0a,
+ 0x0c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a,
+ 0x12, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x22, 0x15, 0x0a, 0x13,
+ 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x03, 0x0a, 0x12, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74,
+ 0x69, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x0b, 0x49, 0x6e,
+ 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x69, 0x6e, 0x74, 0x72,
+ 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31,
+ 0x2e, 0x49, 0x6e, 0x63, 0x72, 0x52, 0x65, 0x61, 0x64, 0x43, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x4c, 0x69, 0x6b, 0x65, 0x12, 0x14, 0x2e, 0x69,
+ 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x6b,
+ 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x61, 0x6e,
+ 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x12, 0x1a, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76,
+ 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61,
+ 0x6e, 0x63, 0x65, 0x6c, 0x4c, 0x69, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x3c, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x12, 0x17, 0x2e, 0x69, 0x6e,
+ 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43,
+ 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30,
+ 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x13, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e,
+ 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6e, 0x74,
+ 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x3f, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x12, 0x18, 0x2e, 0x69,
+ 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76, 0x31,
+ 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x49, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x42, 0x96, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x69, 0x6e, 0x74, 0x72, 0x2e, 0x76,
+ 0x31, 0x42, 0x09, 0x49, 0x6e, 0x74, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f,
+ 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61,
+ 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f,
+ 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
+ 0x2f, 0x69, 0x6e, 0x74, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6e, 0x74, 0x72, 0x76, 0x31, 0xa2,
+ 0x02, 0x03, 0x49, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x2e, 0x56, 0x31, 0xca,
+ 0x02, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x49, 0x6e, 0x74, 0x72,
+ 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea,
+ 0x02, 0x08, 0x49, 0x6e, 0x74, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x33,
+}
+
+var (
+ file_intr_v1_intr_proto_rawDescOnce sync.Once
+ file_intr_v1_intr_proto_rawDescData = file_intr_v1_intr_proto_rawDesc
+)
+
+func file_intr_v1_intr_proto_rawDescGZIP() []byte {
+ file_intr_v1_intr_proto_rawDescOnce.Do(func() {
+ file_intr_v1_intr_proto_rawDescData = protoimpl.X.CompressGZIP(file_intr_v1_intr_proto_rawDescData)
+ })
+ return file_intr_v1_intr_proto_rawDescData
+}
+
+var file_intr_v1_intr_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_intr_v1_intr_proto_goTypes = []interface{}{
+ (*GetByIdsRequest)(nil), // 0: intr.v1.GetByIdsRequest
+ (*GetByIdsResponse)(nil), // 1: intr.v1.GetByIdsResponse
+ (*GetRequest)(nil), // 2: intr.v1.GetRequest
+ (*GetResponse)(nil), // 3: intr.v1.GetResponse
+ (*Interactive)(nil), // 4: intr.v1.Interactive
+ (*CollectRequest)(nil), // 5: intr.v1.CollectRequest
+ (*CollectResponse)(nil), // 6: intr.v1.CollectResponse
+ (*CancelLikeRequest)(nil), // 7: intr.v1.CancelLikeRequest
+ (*CancelLikeResponse)(nil), // 8: intr.v1.CancelLikeResponse
+ (*LikeRequest)(nil), // 9: intr.v1.LikeRequest
+ (*LikeResponse)(nil), // 10: intr.v1.LikeResponse
+ (*IncrReadCntRequest)(nil), // 11: intr.v1.IncrReadCntRequest
+ (*IncrReadCntResponse)(nil), // 12: intr.v1.IncrReadCntResponse
+ nil, // 13: intr.v1.GetByIdsResponse.IntrsEntry
+}
+var file_intr_v1_intr_proto_depIdxs = []int32{
+ 13, // 0: intr.v1.GetByIdsResponse.intrs:type_name -> intr.v1.GetByIdsResponse.IntrsEntry
+ 4, // 1: intr.v1.GetResponse.intr:type_name -> intr.v1.Interactive
+ 4, // 2: intr.v1.GetByIdsResponse.IntrsEntry.value:type_name -> intr.v1.Interactive
+ 11, // 3: intr.v1.InteractiveService.IncrReadCnt:input_type -> intr.v1.IncrReadCntRequest
+ 9, // 4: intr.v1.InteractiveService.Like:input_type -> intr.v1.LikeRequest
+ 7, // 5: intr.v1.InteractiveService.CancelLike:input_type -> intr.v1.CancelLikeRequest
+ 5, // 6: intr.v1.InteractiveService.Collect:input_type -> intr.v1.CollectRequest
+ 2, // 7: intr.v1.InteractiveService.Get:input_type -> intr.v1.GetRequest
+ 0, // 8: intr.v1.InteractiveService.GetByIds:input_type -> intr.v1.GetByIdsRequest
+ 12, // 9: intr.v1.InteractiveService.IncrReadCnt:output_type -> intr.v1.IncrReadCntResponse
+ 10, // 10: intr.v1.InteractiveService.Like:output_type -> intr.v1.LikeResponse
+ 8, // 11: intr.v1.InteractiveService.CancelLike:output_type -> intr.v1.CancelLikeResponse
+ 6, // 12: intr.v1.InteractiveService.Collect:output_type -> intr.v1.CollectResponse
+ 3, // 13: intr.v1.InteractiveService.Get:output_type -> intr.v1.GetResponse
+ 1, // 14: intr.v1.InteractiveService.GetByIds:output_type -> intr.v1.GetByIdsResponse
+ 9, // [9:15] is the sub-list for method output_type
+ 3, // [3:9] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_intr_v1_intr_proto_init() }
+func file_intr_v1_intr_proto_init() {
+ if File_intr_v1_intr_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_intr_v1_intr_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetByIdsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Interactive); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CollectRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CollectResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelLikeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelLikeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LikeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LikeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*IncrReadCntRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_intr_v1_intr_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*IncrReadCntResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_intr_v1_intr_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 14,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_intr_v1_intr_proto_goTypes,
+ DependencyIndexes: file_intr_v1_intr_proto_depIdxs,
+ MessageInfos: file_intr_v1_intr_proto_msgTypes,
+ }.Build()
+ File_intr_v1_intr_proto = out.File
+ file_intr_v1_intr_proto_rawDesc = nil
+ file_intr_v1_intr_proto_goTypes = nil
+ file_intr_v1_intr_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/intr/v1/intr_grpc.pb.go b/webook/api/proto/gen/intr/v1/intr_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..037e85c7efeaaf4e3d207c545bb68197c2258294
--- /dev/null
+++ b/webook/api/proto/gen/intr/v1/intr_grpc.pb.go
@@ -0,0 +1,300 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: intr/v1/intr.proto
+
+package intrv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ InteractiveService_IncrReadCnt_FullMethodName = "/intr.v1.InteractiveService/IncrReadCnt"
+ InteractiveService_Like_FullMethodName = "/intr.v1.InteractiveService/Like"
+ InteractiveService_CancelLike_FullMethodName = "/intr.v1.InteractiveService/CancelLike"
+ InteractiveService_Collect_FullMethodName = "/intr.v1.InteractiveService/Collect"
+ InteractiveService_Get_FullMethodName = "/intr.v1.InteractiveService/Get"
+ InteractiveService_GetByIds_FullMethodName = "/intr.v1.InteractiveService/GetByIds"
+)
+
+// InteractiveServiceClient is the client API for InteractiveService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type InteractiveServiceClient interface {
+ IncrReadCnt(ctx context.Context, in *IncrReadCntRequest, opts ...grpc.CallOption) (*IncrReadCntResponse, error)
+ // Like 点赞
+ Like(ctx context.Context, in *LikeRequest, opts ...grpc.CallOption) (*LikeResponse, error)
+ // CancelLike 取消点赞
+ CancelLike(ctx context.Context, in *CancelLikeRequest, opts ...grpc.CallOption) (*CancelLikeResponse, error)
+ // Collect 收藏
+ Collect(ctx context.Context, in *CollectRequest, opts ...grpc.CallOption) (*CollectResponse, error)
+ Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
+ GetByIds(ctx context.Context, in *GetByIdsRequest, opts ...grpc.CallOption) (*GetByIdsResponse, error)
+}
+
+type interactiveServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewInteractiveServiceClient(cc grpc.ClientConnInterface) InteractiveServiceClient {
+ return &interactiveServiceClient{cc}
+}
+
+func (c *interactiveServiceClient) IncrReadCnt(ctx context.Context, in *IncrReadCntRequest, opts ...grpc.CallOption) (*IncrReadCntResponse, error) {
+ out := new(IncrReadCntResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_IncrReadCnt_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Like(ctx context.Context, in *LikeRequest, opts ...grpc.CallOption) (*LikeResponse, error) {
+ out := new(LikeResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Like_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) CancelLike(ctx context.Context, in *CancelLikeRequest, opts ...grpc.CallOption) (*CancelLikeResponse, error) {
+ out := new(CancelLikeResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_CancelLike_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Collect(ctx context.Context, in *CollectRequest, opts ...grpc.CallOption) (*CollectResponse, error) {
+ out := new(CollectResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Collect_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
+ out := new(GetResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_Get_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *interactiveServiceClient) GetByIds(ctx context.Context, in *GetByIdsRequest, opts ...grpc.CallOption) (*GetByIdsResponse, error) {
+ out := new(GetByIdsResponse)
+ err := c.cc.Invoke(ctx, InteractiveService_GetByIds_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// InteractiveServiceServer is the server API for InteractiveService service.
+// All implementations must embed UnimplementedInteractiveServiceServer
+// for forward compatibility
+type InteractiveServiceServer interface {
+ IncrReadCnt(context.Context, *IncrReadCntRequest) (*IncrReadCntResponse, error)
+ // Like 点赞
+ Like(context.Context, *LikeRequest) (*LikeResponse, error)
+ // CancelLike 取消点赞
+ CancelLike(context.Context, *CancelLikeRequest) (*CancelLikeResponse, error)
+ // Collect 收藏
+ Collect(context.Context, *CollectRequest) (*CollectResponse, error)
+ Get(context.Context, *GetRequest) (*GetResponse, error)
+ GetByIds(context.Context, *GetByIdsRequest) (*GetByIdsResponse, error)
+ mustEmbedUnimplementedInteractiveServiceServer()
+}
+
+// UnimplementedInteractiveServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedInteractiveServiceServer struct {
+}
+
+func (UnimplementedInteractiveServiceServer) IncrReadCnt(context.Context, *IncrReadCntRequest) (*IncrReadCntResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method IncrReadCnt not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Like(context.Context, *LikeRequest) (*LikeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Like not implemented")
+}
+func (UnimplementedInteractiveServiceServer) CancelLike(context.Context, *CancelLikeRequest) (*CancelLikeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CancelLike not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Collect(context.Context, *CollectRequest) (*CollectResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Collect not implemented")
+}
+func (UnimplementedInteractiveServiceServer) Get(context.Context, *GetRequest) (*GetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
+}
+func (UnimplementedInteractiveServiceServer) GetByIds(context.Context, *GetByIdsRequest) (*GetByIdsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetByIds not implemented")
+}
+func (UnimplementedInteractiveServiceServer) mustEmbedUnimplementedInteractiveServiceServer() {}
+
+// UnsafeInteractiveServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to InteractiveServiceServer will
+// result in compilation errors.
+type UnsafeInteractiveServiceServer interface {
+ mustEmbedUnimplementedInteractiveServiceServer()
+}
+
+func RegisterInteractiveServiceServer(s grpc.ServiceRegistrar, srv InteractiveServiceServer) {
+ s.RegisterService(&InteractiveService_ServiceDesc, srv)
+}
+
+func _InteractiveService_IncrReadCnt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(IncrReadCntRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).IncrReadCnt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_IncrReadCnt_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).IncrReadCnt(ctx, req.(*IncrReadCntRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Like_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(LikeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Like(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Like_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Like(ctx, req.(*LikeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_CancelLike_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CancelLikeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).CancelLike(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_CancelLike_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).CancelLike(ctx, req.(*CancelLikeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Collect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CollectRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Collect(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Collect_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Collect(ctx, req.(*CollectRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).Get(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_Get_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).Get(ctx, req.(*GetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _InteractiveService_GetByIds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetByIdsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(InteractiveServiceServer).GetByIds(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: InteractiveService_GetByIds_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(InteractiveServiceServer).GetByIds(ctx, req.(*GetByIdsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// InteractiveService_ServiceDesc is the grpc.ServiceDesc for InteractiveService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var InteractiveService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "intr.v1.InteractiveService",
+ HandlerType: (*InteractiveServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "IncrReadCnt",
+ Handler: _InteractiveService_IncrReadCnt_Handler,
+ },
+ {
+ MethodName: "Like",
+ Handler: _InteractiveService_Like_Handler,
+ },
+ {
+ MethodName: "CancelLike",
+ Handler: _InteractiveService_CancelLike_Handler,
+ },
+ {
+ MethodName: "Collect",
+ Handler: _InteractiveService_Collect_Handler,
+ },
+ {
+ MethodName: "Get",
+ Handler: _InteractiveService_Get_Handler,
+ },
+ {
+ MethodName: "GetByIds",
+ Handler: _InteractiveService_GetByIds_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "intr/v1/intr.proto",
+}
diff --git a/webook/api/proto/gen/intr/v1/mocks/interactive_grpc.mock.go b/webook/api/proto/gen/intr/v1/mocks/interactive_grpc.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..1ea9723d45adfd57fddfba337a0448f12093bd81
--- /dev/null
+++ b/webook/api/proto/gen/intr/v1/mocks/interactive_grpc.mock.go
@@ -0,0 +1,321 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/api/proto/gen/intr/v1/interactive_grpc.pb.go
+//
+// Generated by this command:
+//
+// mockgen -source=webook/api/proto/gen/intr/v1/interactive_grpc.pb.go -package=intrmocks -destination=webook/api/proto/gen/intr/v1/mocks/interactive_grpc.mock.go
+//
+// Package intrmocks is a generated GoMock package.
+package intrmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ gomock "go.uber.org/mock/gomock"
+ grpc "google.golang.org/grpc"
+)
+
+// MockInteractiveServiceClient is a mock of InteractiveServiceClient interface.
+type MockInteractiveServiceClient struct {
+ ctrl *gomock.Controller
+ recorder *MockInteractiveServiceClientMockRecorder
+}
+
+// MockInteractiveServiceClientMockRecorder is the mock recorder for MockInteractiveServiceClient.
+type MockInteractiveServiceClientMockRecorder struct {
+ mock *MockInteractiveServiceClient
+}
+
+// NewMockInteractiveServiceClient creates a new mock instance.
+func NewMockInteractiveServiceClient(ctrl *gomock.Controller) *MockInteractiveServiceClient {
+ mock := &MockInteractiveServiceClient{ctrl: ctrl}
+ mock.recorder = &MockInteractiveServiceClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInteractiveServiceClient) EXPECT() *MockInteractiveServiceClientMockRecorder {
+ return m.recorder
+}
+
+// CancelLike mocks base method.
+func (m *MockInteractiveServiceClient) CancelLike(ctx context.Context, in *intrv1.CancelLikeRequest, opts ...grpc.CallOption) (*intrv1.CancelLikeResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "CancelLike", varargs...)
+ ret0, _ := ret[0].(*intrv1.CancelLikeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CancelLike indicates an expected call of CancelLike.
+func (mr *MockInteractiveServiceClientMockRecorder) CancelLike(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelLike", reflect.TypeOf((*MockInteractiveServiceClient)(nil).CancelLike), varargs...)
+}
+
+// Collect mocks base method.
+func (m *MockInteractiveServiceClient) Collect(ctx context.Context, in *intrv1.CollectRequest, opts ...grpc.CallOption) (*intrv1.CollectResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Collect", varargs...)
+ ret0, _ := ret[0].(*intrv1.CollectResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Collect indicates an expected call of Collect.
+func (mr *MockInteractiveServiceClientMockRecorder) Collect(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collect", reflect.TypeOf((*MockInteractiveServiceClient)(nil).Collect), varargs...)
+}
+
+// Get mocks base method.
+func (m *MockInteractiveServiceClient) Get(ctx context.Context, in *intrv1.GetRequest, opts ...grpc.CallOption) (*intrv1.GetResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Get", varargs...)
+ ret0, _ := ret[0].(*intrv1.GetResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockInteractiveServiceClientMockRecorder) Get(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInteractiveServiceClient)(nil).Get), varargs...)
+}
+
+// GetByIds mocks base method.
+func (m *MockInteractiveServiceClient) GetByIds(ctx context.Context, in *intrv1.GetByIdsRequest, opts ...grpc.CallOption) (*intrv1.GetByIdsResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetByIds", varargs...)
+ ret0, _ := ret[0].(*intrv1.GetByIdsResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetByIds indicates an expected call of GetByIds.
+func (mr *MockInteractiveServiceClientMockRecorder) GetByIds(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIds", reflect.TypeOf((*MockInteractiveServiceClient)(nil).GetByIds), varargs...)
+}
+
+// IncrReadCnt mocks base method.
+func (m *MockInteractiveServiceClient) IncrReadCnt(ctx context.Context, in *intrv1.IncrReadCntRequest, opts ...grpc.CallOption) (*intrv1.IncrReadCntResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "IncrReadCnt", varargs...)
+ ret0, _ := ret[0].(*intrv1.IncrReadCntResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// IncrReadCnt indicates an expected call of IncrReadCnt.
+func (mr *MockInteractiveServiceClientMockRecorder) IncrReadCnt(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrReadCnt", reflect.TypeOf((*MockInteractiveServiceClient)(nil).IncrReadCnt), varargs...)
+}
+
+// Like mocks base method.
+func (m *MockInteractiveServiceClient) Like(ctx context.Context, in *intrv1.LikeRequest, opts ...grpc.CallOption) (*intrv1.LikeResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Like", varargs...)
+ ret0, _ := ret[0].(*intrv1.LikeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Like indicates an expected call of Like.
+func (mr *MockInteractiveServiceClientMockRecorder) Like(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Like", reflect.TypeOf((*MockInteractiveServiceClient)(nil).Like), varargs...)
+}
+
+// MockInteractiveServiceServer is a mock of InteractiveServiceServer interface.
+type MockInteractiveServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockInteractiveServiceServerMockRecorder
+}
+
+// MockInteractiveServiceServerMockRecorder is the mock recorder for MockInteractiveServiceServer.
+type MockInteractiveServiceServerMockRecorder struct {
+ mock *MockInteractiveServiceServer
+}
+
+// NewMockInteractiveServiceServer creates a new mock instance.
+func NewMockInteractiveServiceServer(ctrl *gomock.Controller) *MockInteractiveServiceServer {
+ mock := &MockInteractiveServiceServer{ctrl: ctrl}
+ mock.recorder = &MockInteractiveServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInteractiveServiceServer) EXPECT() *MockInteractiveServiceServerMockRecorder {
+ return m.recorder
+}
+
+// CancelLike mocks base method.
+func (m *MockInteractiveServiceServer) CancelLike(arg0 context.Context, arg1 *intrv1.CancelLikeRequest) (*intrv1.CancelLikeResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CancelLike", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.CancelLikeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CancelLike indicates an expected call of CancelLike.
+func (mr *MockInteractiveServiceServerMockRecorder) CancelLike(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelLike", reflect.TypeOf((*MockInteractiveServiceServer)(nil).CancelLike), arg0, arg1)
+}
+
+// Collect mocks base method.
+func (m *MockInteractiveServiceServer) Collect(arg0 context.Context, arg1 *intrv1.CollectRequest) (*intrv1.CollectResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Collect", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.CollectResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Collect indicates an expected call of Collect.
+func (mr *MockInteractiveServiceServerMockRecorder) Collect(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collect", reflect.TypeOf((*MockInteractiveServiceServer)(nil).Collect), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockInteractiveServiceServer) Get(arg0 context.Context, arg1 *intrv1.GetRequest) (*intrv1.GetResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.GetResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockInteractiveServiceServerMockRecorder) Get(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInteractiveServiceServer)(nil).Get), arg0, arg1)
+}
+
+// GetByIds mocks base method.
+func (m *MockInteractiveServiceServer) GetByIds(arg0 context.Context, arg1 *intrv1.GetByIdsRequest) (*intrv1.GetByIdsResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetByIds", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.GetByIdsResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetByIds indicates an expected call of GetByIds.
+func (mr *MockInteractiveServiceServerMockRecorder) GetByIds(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIds", reflect.TypeOf((*MockInteractiveServiceServer)(nil).GetByIds), arg0, arg1)
+}
+
+// IncrReadCnt mocks base method.
+func (m *MockInteractiveServiceServer) IncrReadCnt(arg0 context.Context, arg1 *intrv1.IncrReadCntRequest) (*intrv1.IncrReadCntResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IncrReadCnt", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.IncrReadCntResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// IncrReadCnt indicates an expected call of IncrReadCnt.
+func (mr *MockInteractiveServiceServerMockRecorder) IncrReadCnt(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrReadCnt", reflect.TypeOf((*MockInteractiveServiceServer)(nil).IncrReadCnt), arg0, arg1)
+}
+
+// Like mocks base method.
+func (m *MockInteractiveServiceServer) Like(arg0 context.Context, arg1 *intrv1.LikeRequest) (*intrv1.LikeResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Like", arg0, arg1)
+ ret0, _ := ret[0].(*intrv1.LikeResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Like indicates an expected call of Like.
+func (mr *MockInteractiveServiceServerMockRecorder) Like(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Like", reflect.TypeOf((*MockInteractiveServiceServer)(nil).Like), arg0, arg1)
+}
+
+// mustEmbedUnimplementedInteractiveServiceServer mocks base method.
+func (m *MockInteractiveServiceServer) mustEmbedUnimplementedInteractiveServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedInteractiveServiceServer")
+}
+
+// mustEmbedUnimplementedInteractiveServiceServer indicates an expected call of mustEmbedUnimplementedInteractiveServiceServer.
+func (mr *MockInteractiveServiceServerMockRecorder) mustEmbedUnimplementedInteractiveServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedInteractiveServiceServer", reflect.TypeOf((*MockInteractiveServiceServer)(nil).mustEmbedUnimplementedInteractiveServiceServer))
+}
+
+// MockUnsafeInteractiveServiceServer is a mock of UnsafeInteractiveServiceServer interface.
+type MockUnsafeInteractiveServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockUnsafeInteractiveServiceServerMockRecorder
+}
+
+// MockUnsafeInteractiveServiceServerMockRecorder is the mock recorder for MockUnsafeInteractiveServiceServer.
+type MockUnsafeInteractiveServiceServerMockRecorder struct {
+ mock *MockUnsafeInteractiveServiceServer
+}
+
+// NewMockUnsafeInteractiveServiceServer creates a new mock instance.
+func NewMockUnsafeInteractiveServiceServer(ctrl *gomock.Controller) *MockUnsafeInteractiveServiceServer {
+ mock := &MockUnsafeInteractiveServiceServer{ctrl: ctrl}
+ mock.recorder = &MockUnsafeInteractiveServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUnsafeInteractiveServiceServer) EXPECT() *MockUnsafeInteractiveServiceServerMockRecorder {
+ return m.recorder
+}
+
+// mustEmbedUnimplementedInteractiveServiceServer mocks base method.
+func (m *MockUnsafeInteractiveServiceServer) mustEmbedUnimplementedInteractiveServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedInteractiveServiceServer")
+}
+
+// mustEmbedUnimplementedInteractiveServiceServer indicates an expected call of mustEmbedUnimplementedInteractiveServiceServer.
+func (mr *MockUnsafeInteractiveServiceServerMockRecorder) mustEmbedUnimplementedInteractiveServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedInteractiveServiceServer", reflect.TypeOf((*MockUnsafeInteractiveServiceServer)(nil).mustEmbedUnimplementedInteractiveServiceServer))
+}
diff --git a/webook/api/proto/gen/oauth2/v1/oauth2.pb.go b/webook/api/proto/gen/oauth2/v1/oauth2.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..743ef9fcf90d0db696a6d6218810d864829ebb55
--- /dev/null
+++ b/webook/api/proto/gen/oauth2/v1/oauth2.pb.go
@@ -0,0 +1,364 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: oauth2/v1/oauth2.proto
+
+package oauth2v1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type AuthURLRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ State string `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"`
+}
+
+func (x *AuthURLRequest) Reset() {
+ *x = AuthURLRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AuthURLRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthURLRequest) ProtoMessage() {}
+
+func (x *AuthURLRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AuthURLRequest.ProtoReflect.Descriptor instead.
+func (*AuthURLRequest) Descriptor() ([]byte, []int) {
+ return file_oauth2_v1_oauth2_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *AuthURLRequest) GetState() string {
+ if x != nil {
+ return x.State
+ }
+ return ""
+}
+
+type AuthURLResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+}
+
+func (x *AuthURLResponse) Reset() {
+ *x = AuthURLResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AuthURLResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthURLResponse) ProtoMessage() {}
+
+func (x *AuthURLResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AuthURLResponse.ProtoReflect.Descriptor instead.
+func (*AuthURLResponse) Descriptor() ([]byte, []int) {
+ return file_oauth2_v1_oauth2_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AuthURLResponse) GetUrl() string {
+ if x != nil {
+ return x.Url
+ }
+ return ""
+}
+
+type VerifyCodeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
+}
+
+func (x *VerifyCodeRequest) Reset() {
+ *x = VerifyCodeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *VerifyCodeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyCodeRequest) ProtoMessage() {}
+
+func (x *VerifyCodeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyCodeRequest.ProtoReflect.Descriptor instead.
+func (*VerifyCodeRequest) Descriptor() ([]byte, []int) {
+ return file_oauth2_v1_oauth2_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *VerifyCodeRequest) GetCode() string {
+ if x != nil {
+ return x.Code
+ }
+ return ""
+}
+
+type VerifyCodeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ OpenId string `protobuf:"bytes,1,opt,name=openId,proto3" json:"openId,omitempty"`
+ UnionId string `protobuf:"bytes,2,opt,name=UnionId,proto3" json:"UnionId,omitempty"`
+}
+
+func (x *VerifyCodeResponse) Reset() {
+ *x = VerifyCodeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *VerifyCodeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VerifyCodeResponse) ProtoMessage() {}
+
+func (x *VerifyCodeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_oauth2_v1_oauth2_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VerifyCodeResponse.ProtoReflect.Descriptor instead.
+func (*VerifyCodeResponse) Descriptor() ([]byte, []int) {
+ return file_oauth2_v1_oauth2_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *VerifyCodeResponse) GetOpenId() string {
+ if x != nil {
+ return x.OpenId
+ }
+ return ""
+}
+
+func (x *VerifyCodeResponse) GetUnionId() string {
+ if x != nil {
+ return x.UnionId
+ }
+ return ""
+}
+
+var File_oauth2_v1_oauth2_proto protoreflect.FileDescriptor
+
+var file_oauth2_v1_oauth2_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x61, 0x75, 0x74,
+ 0x68, 0x32, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32,
+ 0x2e, 0x76, 0x31, 0x22, 0x26, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x23, 0x0a, 0x0f, 0x41,
+ 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10,
+ 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c,
+ 0x22, 0x27, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x46, 0x0a, 0x12, 0x56, 0x65, 0x72,
+ 0x69, 0x66, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x16, 0x0a, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x55, 0x6e, 0x69, 0x6f, 0x6e,
+ 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x55, 0x6e, 0x69, 0x6f, 0x6e, 0x49,
+ 0x64, 0x32, 0x9c, 0x01, 0x0a, 0x0d, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x53, 0x65, 0x72, 0x76,
+ 0x69, 0x63, 0x65, 0x12, 0x40, 0x0a, 0x07, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x12, 0x19,
+ 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x55,
+ 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6f, 0x61, 0x75, 0x74,
+ 0x68, 0x32, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43,
+ 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2e, 0x76, 0x31, 0x2e,
+ 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x1d, 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65,
+ 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x42, 0xa6, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2e,
+ 0x76, 0x31, 0x42, 0x0b, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
+ 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65,
+ 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77,
+ 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+ 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x2f, 0x76, 0x31, 0x3b, 0x6f, 0x61,
+ 0x75, 0x74, 0x68, 0x32, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x4f,
+ 0x61, 0x75, 0x74, 0x68, 0x32, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x09, 0x4f, 0x61, 0x75, 0x74, 0x68,
+ 0x32, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x5c, 0x56, 0x31,
+ 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x4f,
+ 0x61, 0x75, 0x74, 0x68, 0x32, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
+}
+
+var (
+ file_oauth2_v1_oauth2_proto_rawDescOnce sync.Once
+ file_oauth2_v1_oauth2_proto_rawDescData = file_oauth2_v1_oauth2_proto_rawDesc
+)
+
+func file_oauth2_v1_oauth2_proto_rawDescGZIP() []byte {
+ file_oauth2_v1_oauth2_proto_rawDescOnce.Do(func() {
+ file_oauth2_v1_oauth2_proto_rawDescData = protoimpl.X.CompressGZIP(file_oauth2_v1_oauth2_proto_rawDescData)
+ })
+ return file_oauth2_v1_oauth2_proto_rawDescData
+}
+
+var file_oauth2_v1_oauth2_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_oauth2_v1_oauth2_proto_goTypes = []interface{}{
+ (*AuthURLRequest)(nil), // 0: oauth2.v1.AuthURLRequest
+ (*AuthURLResponse)(nil), // 1: oauth2.v1.AuthURLResponse
+ (*VerifyCodeRequest)(nil), // 2: oauth2.v1.VerifyCodeRequest
+ (*VerifyCodeResponse)(nil), // 3: oauth2.v1.VerifyCodeResponse
+}
+var file_oauth2_v1_oauth2_proto_depIdxs = []int32{
+ 0, // 0: oauth2.v1.Oauth2Service.AuthURL:input_type -> oauth2.v1.AuthURLRequest
+ 2, // 1: oauth2.v1.Oauth2Service.VerifyCode:input_type -> oauth2.v1.VerifyCodeRequest
+ 1, // 2: oauth2.v1.Oauth2Service.AuthURL:output_type -> oauth2.v1.AuthURLResponse
+ 3, // 3: oauth2.v1.Oauth2Service.VerifyCode:output_type -> oauth2.v1.VerifyCodeResponse
+ 2, // [2:4] is the sub-list for method output_type
+ 0, // [0:2] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_oauth2_v1_oauth2_proto_init() }
+func file_oauth2_v1_oauth2_proto_init() {
+ if File_oauth2_v1_oauth2_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_oauth2_v1_oauth2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AuthURLRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_oauth2_v1_oauth2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AuthURLResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_oauth2_v1_oauth2_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*VerifyCodeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_oauth2_v1_oauth2_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*VerifyCodeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_oauth2_v1_oauth2_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_oauth2_v1_oauth2_proto_goTypes,
+ DependencyIndexes: file_oauth2_v1_oauth2_proto_depIdxs,
+ MessageInfos: file_oauth2_v1_oauth2_proto_msgTypes,
+ }.Build()
+ File_oauth2_v1_oauth2_proto = out.File
+ file_oauth2_v1_oauth2_proto_rawDesc = nil
+ file_oauth2_v1_oauth2_proto_goTypes = nil
+ file_oauth2_v1_oauth2_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/oauth2/v1/oauth2_grpc.pb.go b/webook/api/proto/gen/oauth2/v1/oauth2_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..d208b0074fa96a46a0474ed019ba0f6a1593b6b6
--- /dev/null
+++ b/webook/api/proto/gen/oauth2/v1/oauth2_grpc.pb.go
@@ -0,0 +1,148 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: oauth2/v1/oauth2.proto
+
+package oauth2v1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ Oauth2Service_AuthURL_FullMethodName = "/oauth2.v1.Oauth2Service/AuthURL"
+ Oauth2Service_VerifyCode_FullMethodName = "/oauth2.v1.Oauth2Service/VerifyCode"
+)
+
+// Oauth2ServiceClient is the client API for Oauth2Service service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type Oauth2ServiceClient interface {
+ // AuthURL 获取验证url
+ AuthURL(ctx context.Context, in *AuthURLRequest, opts ...grpc.CallOption) (*AuthURLResponse, error)
+ VerifyCode(ctx context.Context, in *VerifyCodeRequest, opts ...grpc.CallOption) (*VerifyCodeResponse, error)
+}
+
+type oauth2ServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewOauth2ServiceClient(cc grpc.ClientConnInterface) Oauth2ServiceClient {
+ return &oauth2ServiceClient{cc}
+}
+
+func (c *oauth2ServiceClient) AuthURL(ctx context.Context, in *AuthURLRequest, opts ...grpc.CallOption) (*AuthURLResponse, error) {
+ out := new(AuthURLResponse)
+ err := c.cc.Invoke(ctx, Oauth2Service_AuthURL_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *oauth2ServiceClient) VerifyCode(ctx context.Context, in *VerifyCodeRequest, opts ...grpc.CallOption) (*VerifyCodeResponse, error) {
+ out := new(VerifyCodeResponse)
+ err := c.cc.Invoke(ctx, Oauth2Service_VerifyCode_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// Oauth2ServiceServer is the server API for Oauth2Service service.
+// All implementations must embed UnimplementedOauth2ServiceServer
+// for forward compatibility
+type Oauth2ServiceServer interface {
+ // AuthURL 获取验证url
+ AuthURL(context.Context, *AuthURLRequest) (*AuthURLResponse, error)
+ VerifyCode(context.Context, *VerifyCodeRequest) (*VerifyCodeResponse, error)
+ mustEmbedUnimplementedOauth2ServiceServer()
+}
+
+// UnimplementedOauth2ServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedOauth2ServiceServer struct {
+}
+
+func (UnimplementedOauth2ServiceServer) AuthURL(context.Context, *AuthURLRequest) (*AuthURLResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AuthURL not implemented")
+}
+func (UnimplementedOauth2ServiceServer) VerifyCode(context.Context, *VerifyCodeRequest) (*VerifyCodeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method VerifyCode not implemented")
+}
+func (UnimplementedOauth2ServiceServer) mustEmbedUnimplementedOauth2ServiceServer() {}
+
+// UnsafeOauth2ServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to Oauth2ServiceServer will
+// result in compilation errors.
+type UnsafeOauth2ServiceServer interface {
+ mustEmbedUnimplementedOauth2ServiceServer()
+}
+
+func RegisterOauth2ServiceServer(s grpc.ServiceRegistrar, srv Oauth2ServiceServer) {
+ s.RegisterService(&Oauth2Service_ServiceDesc, srv)
+}
+
+func _Oauth2Service_AuthURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AuthURLRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(Oauth2ServiceServer).AuthURL(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Oauth2Service_AuthURL_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(Oauth2ServiceServer).AuthURL(ctx, req.(*AuthURLRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Oauth2Service_VerifyCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(VerifyCodeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(Oauth2ServiceServer).VerifyCode(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Oauth2Service_VerifyCode_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(Oauth2ServiceServer).VerifyCode(ctx, req.(*VerifyCodeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Oauth2Service_ServiceDesc is the grpc.ServiceDesc for Oauth2Service service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Oauth2Service_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "oauth2.v1.Oauth2Service",
+ HandlerType: (*Oauth2ServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "AuthURL",
+ Handler: _Oauth2Service_AuthURL_Handler,
+ },
+ {
+ MethodName: "VerifyCode",
+ Handler: _Oauth2Service_VerifyCode_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "oauth2/v1/oauth2.proto",
+}
diff --git a/webook/api/proto/gen/payment/v1/mocks/payment_grpc.mock.go b/webook/api/proto/gen/payment/v1/mocks/payment_grpc.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..338925bed45fd88d84a23b5e8bf596652e3813cb
--- /dev/null
+++ b/webook/api/proto/gen/payment/v1/mocks/payment_grpc.mock.go
@@ -0,0 +1,146 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/api/proto/gen/payment/v1/payment_grpc.pb.go
+//
+// Generated by this command:
+//
+// mockgen -source=webook/api/proto/gen/payment/v1/payment_grpc.pb.go -package=pmtmocks -destination=webook/api/proto/gen/payment/v1/mocks/payment_grpc.mock.go
+//
+// Package pmtmocks is a generated GoMock package.
+package pmtmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ gomock "go.uber.org/mock/gomock"
+ grpc "google.golang.org/grpc"
+)
+
+// MockWechatPaymentServiceClient is a mock of WechatPaymentServiceClient interface.
+type MockWechatPaymentServiceClient struct {
+ ctrl *gomock.Controller
+ recorder *MockWechatPaymentServiceClientMockRecorder
+}
+
+// MockWechatPaymentServiceClientMockRecorder is the mock recorder for MockWechatPaymentServiceClient.
+type MockWechatPaymentServiceClientMockRecorder struct {
+ mock *MockWechatPaymentServiceClient
+}
+
+// NewMockWechatPaymentServiceClient creates a new mock instance.
+func NewMockWechatPaymentServiceClient(ctrl *gomock.Controller) *MockWechatPaymentServiceClient {
+ mock := &MockWechatPaymentServiceClient{ctrl: ctrl}
+ mock.recorder = &MockWechatPaymentServiceClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockWechatPaymentServiceClient) EXPECT() *MockWechatPaymentServiceClientMockRecorder {
+ return m.recorder
+}
+
+// NativePrePay mocks base method.
+func (m *MockWechatPaymentServiceClient) NativePrePay(ctx context.Context, in *pmtv1.PrePayRequest, opts ...grpc.CallOption) (*pmtv1.NativePrePayResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx, in}
+ for _, a := range opts {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "NativePrePay", varargs...)
+ ret0, _ := ret[0].(*pmtv1.NativePrePayResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// NativePrePay indicates an expected call of NativePrePay.
+func (mr *MockWechatPaymentServiceClientMockRecorder) NativePrePay(ctx, in any, opts ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx, in}, opts...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NativePrePay", reflect.TypeOf((*MockWechatPaymentServiceClient)(nil).NativePrePay), varargs...)
+}
+
+// MockWechatPaymentServiceServer is a mock of WechatPaymentServiceServer interface.
+type MockWechatPaymentServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockWechatPaymentServiceServerMockRecorder
+}
+
+// MockWechatPaymentServiceServerMockRecorder is the mock recorder for MockWechatPaymentServiceServer.
+type MockWechatPaymentServiceServerMockRecorder struct {
+ mock *MockWechatPaymentServiceServer
+}
+
+// NewMockWechatPaymentServiceServer creates a new mock instance.
+func NewMockWechatPaymentServiceServer(ctrl *gomock.Controller) *MockWechatPaymentServiceServer {
+ mock := &MockWechatPaymentServiceServer{ctrl: ctrl}
+ mock.recorder = &MockWechatPaymentServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockWechatPaymentServiceServer) EXPECT() *MockWechatPaymentServiceServerMockRecorder {
+ return m.recorder
+}
+
+// NativePrePay mocks base method.
+func (m *MockWechatPaymentServiceServer) NativePrePay(arg0 context.Context, arg1 *pmtv1.PrePayRequest) (*pmtv1.NativePrePayResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "NativePrePay", arg0, arg1)
+ ret0, _ := ret[0].(*pmtv1.NativePrePayResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// NativePrePay indicates an expected call of NativePrePay.
+func (mr *MockWechatPaymentServiceServerMockRecorder) NativePrePay(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NativePrePay", reflect.TypeOf((*MockWechatPaymentServiceServer)(nil).NativePrePay), arg0, arg1)
+}
+
+// mustEmbedUnimplementedWechatPaymentServiceServer mocks base method.
+func (m *MockWechatPaymentServiceServer) mustEmbedUnimplementedWechatPaymentServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedWechatPaymentServiceServer")
+}
+
+// mustEmbedUnimplementedWechatPaymentServiceServer indicates an expected call of mustEmbedUnimplementedWechatPaymentServiceServer.
+func (mr *MockWechatPaymentServiceServerMockRecorder) mustEmbedUnimplementedWechatPaymentServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedWechatPaymentServiceServer", reflect.TypeOf((*MockWechatPaymentServiceServer)(nil).mustEmbedUnimplementedWechatPaymentServiceServer))
+}
+
+// MockUnsafeWechatPaymentServiceServer is a mock of UnsafeWechatPaymentServiceServer interface.
+type MockUnsafeWechatPaymentServiceServer struct {
+ ctrl *gomock.Controller
+ recorder *MockUnsafeWechatPaymentServiceServerMockRecorder
+}
+
+// MockUnsafeWechatPaymentServiceServerMockRecorder is the mock recorder for MockUnsafeWechatPaymentServiceServer.
+type MockUnsafeWechatPaymentServiceServerMockRecorder struct {
+ mock *MockUnsafeWechatPaymentServiceServer
+}
+
+// NewMockUnsafeWechatPaymentServiceServer creates a new mock instance.
+func NewMockUnsafeWechatPaymentServiceServer(ctrl *gomock.Controller) *MockUnsafeWechatPaymentServiceServer {
+ mock := &MockUnsafeWechatPaymentServiceServer{ctrl: ctrl}
+ mock.recorder = &MockUnsafeWechatPaymentServiceServerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUnsafeWechatPaymentServiceServer) EXPECT() *MockUnsafeWechatPaymentServiceServerMockRecorder {
+ return m.recorder
+}
+
+// mustEmbedUnimplementedWechatPaymentServiceServer mocks base method.
+func (m *MockUnsafeWechatPaymentServiceServer) mustEmbedUnimplementedWechatPaymentServiceServer() {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "mustEmbedUnimplementedWechatPaymentServiceServer")
+}
+
+// mustEmbedUnimplementedWechatPaymentServiceServer indicates an expected call of mustEmbedUnimplementedWechatPaymentServiceServer.
+func (mr *MockUnsafeWechatPaymentServiceServerMockRecorder) mustEmbedUnimplementedWechatPaymentServiceServer() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedWechatPaymentServiceServer", reflect.TypeOf((*MockUnsafeWechatPaymentServiceServer)(nil).mustEmbedUnimplementedWechatPaymentServiceServer))
+}
diff --git a/webook/api/proto/gen/payment/v1/payment.pb.go b/webook/api/proto/gen/payment/v1/payment.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec1dfa0582b20f919baf51b555213ef06caf4825
--- /dev/null
+++ b/webook/api/proto/gen/payment/v1/payment.pb.go
@@ -0,0 +1,523 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: payment/v1/payment.proto
+
+// buf:lint:ignore PACKAGE_DIRECTORY_MATCH
+
+package pmtv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PaymentStatus int32
+
+const (
+ PaymentStatus_PaymentStatusUnknown PaymentStatus = 0
+ PaymentStatus_PaymentStatusInit PaymentStatus = 1
+ PaymentStatus_PaymentStatusSuccess PaymentStatus = 2
+ PaymentStatus_PaymentStatusFailed PaymentStatus = 3
+ PaymentStatus_PaymentStatusRefund PaymentStatus = 4
+)
+
+// Enum value maps for PaymentStatus.
+var (
+ PaymentStatus_name = map[int32]string{
+ 0: "PaymentStatusUnknown",
+ 1: "PaymentStatusInit",
+ 2: "PaymentStatusSuccess",
+ 3: "PaymentStatusFailed",
+ 4: "PaymentStatusRefund",
+ }
+ PaymentStatus_value = map[string]int32{
+ "PaymentStatusUnknown": 0,
+ "PaymentStatusInit": 1,
+ "PaymentStatusSuccess": 2,
+ "PaymentStatusFailed": 3,
+ "PaymentStatusRefund": 4,
+ }
+)
+
+func (x PaymentStatus) Enum() *PaymentStatus {
+ p := new(PaymentStatus)
+ *p = x
+ return p
+}
+
+func (x PaymentStatus) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (PaymentStatus) Descriptor() protoreflect.EnumDescriptor {
+ return file_payment_v1_payment_proto_enumTypes[0].Descriptor()
+}
+
+func (PaymentStatus) Type() protoreflect.EnumType {
+ return &file_payment_v1_payment_proto_enumTypes[0]
+}
+
+func (x PaymentStatus) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use PaymentStatus.Descriptor instead.
+func (PaymentStatus) EnumDescriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{0}
+}
+
+type GetPaymentRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ BizTradeNo string `protobuf:"bytes,1,opt,name=biz_trade_no,json=bizTradeNo,proto3" json:"biz_trade_no,omitempty"`
+}
+
+func (x *GetPaymentRequest) Reset() {
+ *x = GetPaymentRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_payment_v1_payment_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetPaymentRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPaymentRequest) ProtoMessage() {}
+
+func (x *GetPaymentRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_payment_v1_payment_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPaymentRequest.ProtoReflect.Descriptor instead.
+func (*GetPaymentRequest) Descriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetPaymentRequest) GetBizTradeNo() string {
+ if x != nil {
+ return x.BizTradeNo
+ }
+ return ""
+}
+
+type GetPaymentResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 有需要再加字段
+ Status PaymentStatus `protobuf:"varint,2,opt,name=status,proto3,enum=pmt.v1.PaymentStatus" json:"status,omitempty"`
+}
+
+func (x *GetPaymentResponse) Reset() {
+ *x = GetPaymentResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_payment_v1_payment_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetPaymentResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPaymentResponse) ProtoMessage() {}
+
+func (x *GetPaymentResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_payment_v1_payment_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPaymentResponse.ProtoReflect.Descriptor instead.
+func (*GetPaymentResponse) Descriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetPaymentResponse) GetStatus() PaymentStatus {
+ if x != nil {
+ return x.Status
+ }
+ return PaymentStatus_PaymentStatusUnknown
+}
+
+type PrePayRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Amt *Amount `protobuf:"bytes,1,opt,name=amt,proto3" json:"amt,omitempty"`
+ BizTradeNo string `protobuf:"bytes,2,opt,name=biz_trade_no,json=bizTradeNo,proto3" json:"biz_trade_no,omitempty"`
+ Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
+}
+
+func (x *PrePayRequest) Reset() {
+ *x = PrePayRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_payment_v1_payment_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PrePayRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PrePayRequest) ProtoMessage() {}
+
+func (x *PrePayRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_payment_v1_payment_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PrePayRequest.ProtoReflect.Descriptor instead.
+func (*PrePayRequest) Descriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *PrePayRequest) GetAmt() *Amount {
+ if x != nil {
+ return x.Amt
+ }
+ return nil
+}
+
+func (x *PrePayRequest) GetBizTradeNo() string {
+ if x != nil {
+ return x.BizTradeNo
+ }
+ return ""
+}
+
+func (x *PrePayRequest) GetDescription() string {
+ if x != nil {
+ return x.Description
+ }
+ return ""
+}
+
+type Amount struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Total int64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
+ Currency string `protobuf:"bytes,2,opt,name=currency,proto3" json:"currency,omitempty"`
+}
+
+func (x *Amount) Reset() {
+ *x = Amount{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_payment_v1_payment_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Amount) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Amount) ProtoMessage() {}
+
+func (x *Amount) ProtoReflect() protoreflect.Message {
+ mi := &file_payment_v1_payment_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Amount.ProtoReflect.Descriptor instead.
+func (*Amount) Descriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Amount) GetTotal() int64 {
+ if x != nil {
+ return x.Total
+ }
+ return 0
+}
+
+func (x *Amount) GetCurrency() string {
+ if x != nil {
+ return x.Currency
+ }
+ return ""
+}
+
+// NativePrePayResponse 的 response 因为支付方式不同,
+// 所以响应的含义也会有不同。
+type NativePrePayResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ CodeUrl string `protobuf:"bytes,1,opt,name=code_url,json=codeUrl,proto3" json:"code_url,omitempty"`
+}
+
+func (x *NativePrePayResponse) Reset() {
+ *x = NativePrePayResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_payment_v1_payment_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *NativePrePayResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NativePrePayResponse) ProtoMessage() {}
+
+func (x *NativePrePayResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_payment_v1_payment_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use NativePrePayResponse.ProtoReflect.Descriptor instead.
+func (*NativePrePayResponse) Descriptor() ([]byte, []int) {
+ return file_payment_v1_payment_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *NativePrePayResponse) GetCodeUrl() string {
+ if x != nil {
+ return x.CodeUrl
+ }
+ return ""
+}
+
+var File_payment_v1_payment_proto protoreflect.FileDescriptor
+
+var file_payment_v1_payment_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79,
+ 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, 0x6d, 0x74, 0x2e,
+ 0x76, 0x31, 0x22, 0x35, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x62, 0x69, 0x7a, 0x5f, 0x74,
+ 0x72, 0x61, 0x64, 0x65, 0x5f, 0x6e, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62,
+ 0x69, 0x7a, 0x54, 0x72, 0x61, 0x64, 0x65, 0x4e, 0x6f, 0x22, 0x43, 0x0a, 0x12, 0x47, 0x65, 0x74,
+ 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x2d, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
+ 0x15, 0x2e, 0x70, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74,
+ 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x75,
+ 0x0a, 0x0d, 0x50, 0x72, 0x65, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x20, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70,
+ 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x03, 0x61, 0x6d,
+ 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x62, 0x69, 0x7a, 0x5f, 0x74, 0x72, 0x61, 0x64, 0x65, 0x5f, 0x6e,
+ 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x69, 0x7a, 0x54, 0x72, 0x61, 0x64,
+ 0x65, 0x4e, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69,
+ 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x0a, 0x06, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12,
+ 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63,
+ 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63,
+ 0x79, 0x22, 0x31, 0x0a, 0x14, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x72, 0x65, 0x50, 0x61,
+ 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6f, 0x64,
+ 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x64,
+ 0x65, 0x55, 0x72, 0x6c, 0x2a, 0x8c, 0x01, 0x0a, 0x0d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74,
+ 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e,
+ 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00,
+ 0x12, 0x15, 0x0a, 0x11, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x49, 0x6e, 0x69, 0x74, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, 0x65,
+ 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x10,
+ 0x02, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x61,
+ 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x66, 0x75, 0x6e,
+ 0x64, 0x10, 0x04, 0x32, 0xa0, 0x01, 0x0a, 0x14, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x50, 0x61,
+ 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0c,
+ 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x72, 0x65, 0x50, 0x61, 0x79, 0x12, 0x15, 0x2e, 0x70,
+ 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x74,
+ 0x69, 0x76, 0x65, 0x50, 0x72, 0x65, 0x50, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12,
+ 0x19, 0x2e, 0x70, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x61, 0x79, 0x6d,
+ 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x6d, 0x74,
+ 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x96, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70,
+ 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72,
+ 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d,
+ 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f,
+ 0x76, 0x31, 0x3b, 0x70, 0x6d, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02,
+ 0x06, 0x50, 0x6d, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x50, 0x6d, 0x74, 0x5c, 0x56, 0x31,
+ 0xe2, 0x02, 0x12, 0x50, 0x6d, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x50, 0x6d, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62,
+ 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_payment_v1_payment_proto_rawDescOnce sync.Once
+ file_payment_v1_payment_proto_rawDescData = file_payment_v1_payment_proto_rawDesc
+)
+
+func file_payment_v1_payment_proto_rawDescGZIP() []byte {
+ file_payment_v1_payment_proto_rawDescOnce.Do(func() {
+ file_payment_v1_payment_proto_rawDescData = protoimpl.X.CompressGZIP(file_payment_v1_payment_proto_rawDescData)
+ })
+ return file_payment_v1_payment_proto_rawDescData
+}
+
+var file_payment_v1_payment_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_payment_v1_payment_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_payment_v1_payment_proto_goTypes = []interface{}{
+ (PaymentStatus)(0), // 0: pmt.v1.PaymentStatus
+ (*GetPaymentRequest)(nil), // 1: pmt.v1.GetPaymentRequest
+ (*GetPaymentResponse)(nil), // 2: pmt.v1.GetPaymentResponse
+ (*PrePayRequest)(nil), // 3: pmt.v1.PrePayRequest
+ (*Amount)(nil), // 4: pmt.v1.Amount
+ (*NativePrePayResponse)(nil), // 5: pmt.v1.NativePrePayResponse
+}
+var file_payment_v1_payment_proto_depIdxs = []int32{
+ 0, // 0: pmt.v1.GetPaymentResponse.status:type_name -> pmt.v1.PaymentStatus
+ 4, // 1: pmt.v1.PrePayRequest.amt:type_name -> pmt.v1.Amount
+ 3, // 2: pmt.v1.WechatPaymentService.NativePrePay:input_type -> pmt.v1.PrePayRequest
+ 1, // 3: pmt.v1.WechatPaymentService.GetPayment:input_type -> pmt.v1.GetPaymentRequest
+ 5, // 4: pmt.v1.WechatPaymentService.NativePrePay:output_type -> pmt.v1.NativePrePayResponse
+ 2, // 5: pmt.v1.WechatPaymentService.GetPayment:output_type -> pmt.v1.GetPaymentResponse
+ 4, // [4:6] is the sub-list for method output_type
+ 2, // [2:4] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_payment_v1_payment_proto_init() }
+func file_payment_v1_payment_proto_init() {
+ if File_payment_v1_payment_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_payment_v1_payment_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetPaymentRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_payment_v1_payment_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetPaymentResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_payment_v1_payment_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PrePayRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_payment_v1_payment_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Amount); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_payment_v1_payment_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*NativePrePayResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_payment_v1_payment_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 5,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_payment_v1_payment_proto_goTypes,
+ DependencyIndexes: file_payment_v1_payment_proto_depIdxs,
+ EnumInfos: file_payment_v1_payment_proto_enumTypes,
+ MessageInfos: file_payment_v1_payment_proto_msgTypes,
+ }.Build()
+ File_payment_v1_payment_proto = out.File
+ file_payment_v1_payment_proto_rawDesc = nil
+ file_payment_v1_payment_proto_goTypes = nil
+ file_payment_v1_payment_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/payment/v1/payment_grpc.pb.go b/webook/api/proto/gen/payment/v1/payment_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..b33d2d195f579310534f9c37e5a1c3c2696fb58e
--- /dev/null
+++ b/webook/api/proto/gen/payment/v1/payment_grpc.pb.go
@@ -0,0 +1,150 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: payment/v1/payment.proto
+
+// buf:lint:ignore PACKAGE_DIRECTORY_MATCH
+
+package pmtv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ WechatPaymentService_NativePrePay_FullMethodName = "/pmt.v1.WechatPaymentService/NativePrePay"
+ WechatPaymentService_GetPayment_FullMethodName = "/pmt.v1.WechatPaymentService/GetPayment"
+)
+
+// WechatPaymentServiceClient is the client API for WechatPaymentService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type WechatPaymentServiceClient interface {
+ // buf:lint:ignore RPC_REQUEST_STANDARD_NAME
+ NativePrePay(ctx context.Context, in *PrePayRequest, opts ...grpc.CallOption) (*NativePrePayResponse, error)
+ GetPayment(ctx context.Context, in *GetPaymentRequest, opts ...grpc.CallOption) (*GetPaymentResponse, error)
+}
+
+type wechatPaymentServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewWechatPaymentServiceClient(cc grpc.ClientConnInterface) WechatPaymentServiceClient {
+ return &wechatPaymentServiceClient{cc}
+}
+
+func (c *wechatPaymentServiceClient) NativePrePay(ctx context.Context, in *PrePayRequest, opts ...grpc.CallOption) (*NativePrePayResponse, error) {
+ out := new(NativePrePayResponse)
+ err := c.cc.Invoke(ctx, WechatPaymentService_NativePrePay_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *wechatPaymentServiceClient) GetPayment(ctx context.Context, in *GetPaymentRequest, opts ...grpc.CallOption) (*GetPaymentResponse, error) {
+ out := new(GetPaymentResponse)
+ err := c.cc.Invoke(ctx, WechatPaymentService_GetPayment_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// WechatPaymentServiceServer is the server API for WechatPaymentService service.
+// All implementations must embed UnimplementedWechatPaymentServiceServer
+// for forward compatibility
+type WechatPaymentServiceServer interface {
+ // buf:lint:ignore RPC_REQUEST_STANDARD_NAME
+ NativePrePay(context.Context, *PrePayRequest) (*NativePrePayResponse, error)
+ GetPayment(context.Context, *GetPaymentRequest) (*GetPaymentResponse, error)
+ mustEmbedUnimplementedWechatPaymentServiceServer()
+}
+
+// UnimplementedWechatPaymentServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedWechatPaymentServiceServer struct {
+}
+
+func (UnimplementedWechatPaymentServiceServer) NativePrePay(context.Context, *PrePayRequest) (*NativePrePayResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method NativePrePay not implemented")
+}
+func (UnimplementedWechatPaymentServiceServer) GetPayment(context.Context, *GetPaymentRequest) (*GetPaymentResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetPayment not implemented")
+}
+func (UnimplementedWechatPaymentServiceServer) mustEmbedUnimplementedWechatPaymentServiceServer() {}
+
+// UnsafeWechatPaymentServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to WechatPaymentServiceServer will
+// result in compilation errors.
+type UnsafeWechatPaymentServiceServer interface {
+ mustEmbedUnimplementedWechatPaymentServiceServer()
+}
+
+func RegisterWechatPaymentServiceServer(s grpc.ServiceRegistrar, srv WechatPaymentServiceServer) {
+ s.RegisterService(&WechatPaymentService_ServiceDesc, srv)
+}
+
+func _WechatPaymentService_NativePrePay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(PrePayRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(WechatPaymentServiceServer).NativePrePay(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: WechatPaymentService_NativePrePay_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(WechatPaymentServiceServer).NativePrePay(ctx, req.(*PrePayRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _WechatPaymentService_GetPayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetPaymentRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(WechatPaymentServiceServer).GetPayment(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: WechatPaymentService_GetPayment_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(WechatPaymentServiceServer).GetPayment(ctx, req.(*GetPaymentRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// WechatPaymentService_ServiceDesc is the grpc.ServiceDesc for WechatPaymentService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var WechatPaymentService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "pmt.v1.WechatPaymentService",
+ HandlerType: (*WechatPaymentServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "NativePrePay",
+ Handler: _WechatPaymentService_NativePrePay_Handler,
+ },
+ {
+ MethodName: "GetPayment",
+ Handler: _WechatPaymentService_GetPayment_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "payment/v1/payment.proto",
+}
diff --git a/webook/api/proto/gen/ranking/v1/ranking.pb.go b/webook/api/proto/gen/ranking/v1/ranking.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..0b9e1dace0920b1557a61d68fdbfe487468aa76f
--- /dev/null
+++ b/webook/api/proto/gen/ranking/v1/ranking.pb.go
@@ -0,0 +1,527 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: ranking/v1/ranking.proto
+
+package rankingv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Author struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 添加其他作者相关字段
+}
+
+func (x *Author) Reset() {
+ *x = Author{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Author) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Author) ProtoMessage() {}
+
+func (x *Author) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Author.ProtoReflect.Descriptor instead.
+func (*Author) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Author) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Author) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type Article struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+ Status int32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"`
+ Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+ Author *Author `protobuf:"bytes,5,opt,name=author,proto3" json:"author,omitempty"`
+ Ctime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=ctime,proto3" json:"ctime,omitempty"`
+ Utime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=utime,proto3" json:"utime,omitempty"`
+}
+
+func (x *Article) Reset() {
+ *x = Article{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Article) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *Article) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Article) GetTitle() string {
+ if x != nil {
+ return x.Title
+ }
+ return ""
+}
+
+func (x *Article) GetStatus() int32 {
+ if x != nil {
+ return x.Status
+ }
+ return 0
+}
+
+func (x *Article) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *Article) GetAuthor() *Author {
+ if x != nil {
+ return x.Author
+ }
+ return nil
+}
+
+func (x *Article) GetCtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Ctime
+ }
+ return nil
+}
+
+func (x *Article) GetUtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Utime
+ }
+ return nil
+}
+
+type RankTopNRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *RankTopNRequest) Reset() {
+ *x = RankTopNRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RankTopNRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RankTopNRequest) ProtoMessage() {}
+
+func (x *RankTopNRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RankTopNRequest.ProtoReflect.Descriptor instead.
+func (*RankTopNRequest) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{2}
+}
+
+type RankTopNResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *RankTopNResponse) Reset() {
+ *x = RankTopNResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RankTopNResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RankTopNResponse) ProtoMessage() {}
+
+func (x *RankTopNResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RankTopNResponse.ProtoReflect.Descriptor instead.
+func (*RankTopNResponse) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{3}
+}
+
+type TopNRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *TopNRequest) Reset() {
+ *x = TopNRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *TopNRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TopNRequest) ProtoMessage() {}
+
+func (x *TopNRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TopNRequest.ProtoReflect.Descriptor instead.
+func (*TopNRequest) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{4}
+}
+
+type TopNResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Articles []*Article `protobuf:"bytes,1,rep,name=articles,proto3" json:"articles,omitempty"`
+}
+
+func (x *TopNResponse) Reset() {
+ *x = TopNResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *TopNResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TopNResponse) ProtoMessage() {}
+
+func (x *TopNResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_ranking_v1_ranking_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TopNResponse.ProtoReflect.Descriptor instead.
+func (*TopNResponse) Descriptor() ([]byte, []int) {
+ return file_ranking_v1_ranking_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *TopNResponse) GetArticles() []*Article {
+ if x != nil {
+ return x.Articles
+ }
+ return nil
+}
+
+var File_ranking_v1_ranking_proto protoreflect.FileDescriptor
+
+var file_ranking_v1_ranking_proto_rawDesc = []byte{
+ 0x0a, 0x18, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x61, 0x6e,
+ 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x72, 0x61, 0x6e, 0x6b,
+ 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x06, 0x41, 0x75, 0x74, 0x68, 0x6f,
+ 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c,
+ 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+ 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x75, 0x74,
+ 0x68, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x61, 0x6e, 0x6b,
+ 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x52, 0x06, 0x61,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x30, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x74, 0x69, 0x6d, 0x65,
+ 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+ 0x6d, 0x70, 0x52, 0x05, 0x75, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x61, 0x6e,
+ 0x6b, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10,
+ 0x52, 0x61, 0x6e, 0x6b, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x22, 0x0d, 0x0a, 0x0b, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
+ 0x3f, 0x0a, 0x0c, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x2f, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+ 0x0b, 0x32, 0x13, 0x2e, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41,
+ 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73,
+ 0x32, 0x96, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76,
+ 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x08, 0x52, 0x61, 0x6e, 0x6b, 0x54, 0x6f, 0x70, 0x4e, 0x12,
+ 0x1b, 0x2e, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x6e,
+ 0x6b, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x72,
+ 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x6e, 0x6b, 0x54, 0x6f,
+ 0x70, 0x4e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x04,
+ 0x54, 0x6f, 0x70, 0x4e, 0x12, 0x17, 0x2e, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
+ 0x31, 0x2e, 0x54, 0x6f, 0x70, 0x4e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e,
+ 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x70, 0x4e, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xae, 0x01, 0x0a, 0x0e, 0x63, 0x6f,
+ 0x6d, 0x2e, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x52, 0x61,
+ 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69,
+ 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67,
+ 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b,
+ 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x72,
+ 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x61, 0x6e, 0x6b, 0x69, 0x6e,
+ 0x67, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x52, 0x61, 0x6e, 0x6b,
+ 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0a, 0x52, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67,
+ 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x16, 0x52, 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31,
+ 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0b, 0x52,
+ 0x61, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x33,
+}
+
+var (
+ file_ranking_v1_ranking_proto_rawDescOnce sync.Once
+ file_ranking_v1_ranking_proto_rawDescData = file_ranking_v1_ranking_proto_rawDesc
+)
+
+func file_ranking_v1_ranking_proto_rawDescGZIP() []byte {
+ file_ranking_v1_ranking_proto_rawDescOnce.Do(func() {
+ file_ranking_v1_ranking_proto_rawDescData = protoimpl.X.CompressGZIP(file_ranking_v1_ranking_proto_rawDescData)
+ })
+ return file_ranking_v1_ranking_proto_rawDescData
+}
+
+var file_ranking_v1_ranking_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_ranking_v1_ranking_proto_goTypes = []interface{}{
+ (*Author)(nil), // 0: ranking.v1.Author
+ (*Article)(nil), // 1: ranking.v1.Article
+ (*RankTopNRequest)(nil), // 2: ranking.v1.RankTopNRequest
+ (*RankTopNResponse)(nil), // 3: ranking.v1.RankTopNResponse
+ (*TopNRequest)(nil), // 4: ranking.v1.TopNRequest
+ (*TopNResponse)(nil), // 5: ranking.v1.TopNResponse
+ (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
+}
+var file_ranking_v1_ranking_proto_depIdxs = []int32{
+ 0, // 0: ranking.v1.Article.author:type_name -> ranking.v1.Author
+ 6, // 1: ranking.v1.Article.ctime:type_name -> google.protobuf.Timestamp
+ 6, // 2: ranking.v1.Article.utime:type_name -> google.protobuf.Timestamp
+ 1, // 3: ranking.v1.TopNResponse.articles:type_name -> ranking.v1.Article
+ 2, // 4: ranking.v1.RankingService.RankTopN:input_type -> ranking.v1.RankTopNRequest
+ 4, // 5: ranking.v1.RankingService.TopN:input_type -> ranking.v1.TopNRequest
+ 3, // 6: ranking.v1.RankingService.RankTopN:output_type -> ranking.v1.RankTopNResponse
+ 5, // 7: ranking.v1.RankingService.TopN:output_type -> ranking.v1.TopNResponse
+ 6, // [6:8] is the sub-list for method output_type
+ 4, // [4:6] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_ranking_v1_ranking_proto_init() }
+func file_ranking_v1_ranking_proto_init() {
+ if File_ranking_v1_ranking_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_ranking_v1_ranking_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Author); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_ranking_v1_ranking_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Article); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_ranking_v1_ranking_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RankTopNRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_ranking_v1_ranking_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RankTopNResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_ranking_v1_ranking_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*TopNRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_ranking_v1_ranking_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*TopNResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_ranking_v1_ranking_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 6,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_ranking_v1_ranking_proto_goTypes,
+ DependencyIndexes: file_ranking_v1_ranking_proto_depIdxs,
+ MessageInfos: file_ranking_v1_ranking_proto_msgTypes,
+ }.Build()
+ File_ranking_v1_ranking_proto = out.File
+ file_ranking_v1_ranking_proto_rawDesc = nil
+ file_ranking_v1_ranking_proto_goTypes = nil
+ file_ranking_v1_ranking_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/ranking/v1/ranking_grpc.pb.go b/webook/api/proto/gen/ranking/v1/ranking_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..81ed6a0c9b4a58300413865481af0d759e4775d0
--- /dev/null
+++ b/webook/api/proto/gen/ranking/v1/ranking_grpc.pb.go
@@ -0,0 +1,146 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: ranking/v1/ranking.proto
+
+package rankingv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ RankingService_RankTopN_FullMethodName = "/ranking.v1.RankingService/RankTopN"
+ RankingService_TopN_FullMethodName = "/ranking.v1.RankingService/TopN"
+)
+
+// RankingServiceClient is the client API for RankingService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type RankingServiceClient interface {
+ RankTopN(ctx context.Context, in *RankTopNRequest, opts ...grpc.CallOption) (*RankTopNResponse, error)
+ TopN(ctx context.Context, in *TopNRequest, opts ...grpc.CallOption) (*TopNResponse, error)
+}
+
+type rankingServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewRankingServiceClient(cc grpc.ClientConnInterface) RankingServiceClient {
+ return &rankingServiceClient{cc}
+}
+
+func (c *rankingServiceClient) RankTopN(ctx context.Context, in *RankTopNRequest, opts ...grpc.CallOption) (*RankTopNResponse, error) {
+ out := new(RankTopNResponse)
+ err := c.cc.Invoke(ctx, RankingService_RankTopN_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *rankingServiceClient) TopN(ctx context.Context, in *TopNRequest, opts ...grpc.CallOption) (*TopNResponse, error) {
+ out := new(TopNResponse)
+ err := c.cc.Invoke(ctx, RankingService_TopN_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// RankingServiceServer is the server API for RankingService service.
+// All implementations must embed UnimplementedRankingServiceServer
+// for forward compatibility
+type RankingServiceServer interface {
+ RankTopN(context.Context, *RankTopNRequest) (*RankTopNResponse, error)
+ TopN(context.Context, *TopNRequest) (*TopNResponse, error)
+ mustEmbedUnimplementedRankingServiceServer()
+}
+
+// UnimplementedRankingServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedRankingServiceServer struct {
+}
+
+func (UnimplementedRankingServiceServer) RankTopN(context.Context, *RankTopNRequest) (*RankTopNResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RankTopN not implemented")
+}
+func (UnimplementedRankingServiceServer) TopN(context.Context, *TopNRequest) (*TopNResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method TopN not implemented")
+}
+func (UnimplementedRankingServiceServer) mustEmbedUnimplementedRankingServiceServer() {}
+
+// UnsafeRankingServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to RankingServiceServer will
+// result in compilation errors.
+type UnsafeRankingServiceServer interface {
+ mustEmbedUnimplementedRankingServiceServer()
+}
+
+func RegisterRankingServiceServer(s grpc.ServiceRegistrar, srv RankingServiceServer) {
+ s.RegisterService(&RankingService_ServiceDesc, srv)
+}
+
+func _RankingService_RankTopN_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RankTopNRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RankingServiceServer).RankTopN(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: RankingService_RankTopN_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RankingServiceServer).RankTopN(ctx, req.(*RankTopNRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _RankingService_TopN_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(TopNRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RankingServiceServer).TopN(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: RankingService_TopN_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RankingServiceServer).TopN(ctx, req.(*TopNRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// RankingService_ServiceDesc is the grpc.ServiceDesc for RankingService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var RankingService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "ranking.v1.RankingService",
+ HandlerType: (*RankingServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "RankTopN",
+ Handler: _RankingService_RankTopN_Handler,
+ },
+ {
+ MethodName: "TopN",
+ Handler: _RankingService_TopN_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "ranking/v1/ranking.proto",
+}
diff --git a/webook/api/proto/gen/reward/v1/reward.pb.go b/webook/api/proto/gen/reward/v1/reward.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..1470067a7f39b3951057621f68cd27abe8f68da6
--- /dev/null
+++ b/webook/api/proto/gen/reward/v1/reward.pb.go
@@ -0,0 +1,496 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: reward/v1/reward.proto
+
+package rewardv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type RewardStatus int32
+
+const (
+ RewardStatus_RewardStatusUnknown RewardStatus = 0
+ RewardStatus_RewardStatusInit RewardStatus = 1
+ RewardStatus_RewardStatusPayed RewardStatus = 2
+ RewardStatus_RewardStatusFailed RewardStatus = 3
+)
+
+// Enum value maps for RewardStatus.
+var (
+ RewardStatus_name = map[int32]string{
+ 0: "RewardStatusUnknown",
+ 1: "RewardStatusInit",
+ 2: "RewardStatusPayed",
+ 3: "RewardStatusFailed",
+ }
+ RewardStatus_value = map[string]int32{
+ "RewardStatusUnknown": 0,
+ "RewardStatusInit": 1,
+ "RewardStatusPayed": 2,
+ "RewardStatusFailed": 3,
+ }
+)
+
+func (x RewardStatus) Enum() *RewardStatus {
+ p := new(RewardStatus)
+ *p = x
+ return p
+}
+
+func (x RewardStatus) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (RewardStatus) Descriptor() protoreflect.EnumDescriptor {
+ return file_reward_v1_reward_proto_enumTypes[0].Descriptor()
+}
+
+func (RewardStatus) Type() protoreflect.EnumType {
+ return &file_reward_v1_reward_proto_enumTypes[0]
+}
+
+func (x RewardStatus) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use RewardStatus.Descriptor instead.
+func (RewardStatus) EnumDescriptor() ([]byte, []int) {
+ return file_reward_v1_reward_proto_rawDescGZIP(), []int{0}
+}
+
+type GetRewardRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // rid 和 打赏的人
+ Rid int64 `protobuf:"varint,1,opt,name=rid,proto3" json:"rid,omitempty"`
+ Uid int64 `protobuf:"varint,2,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *GetRewardRequest) Reset() {
+ *x = GetRewardRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_reward_v1_reward_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetRewardRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetRewardRequest) ProtoMessage() {}
+
+func (x *GetRewardRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_reward_v1_reward_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetRewardRequest.ProtoReflect.Descriptor instead.
+func (*GetRewardRequest) Descriptor() ([]byte, []int) {
+ return file_reward_v1_reward_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetRewardRequest) GetRid() int64 {
+ if x != nil {
+ return x.Rid
+ }
+ return 0
+}
+
+func (x *GetRewardRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+// 正常来说,对于外面的人来说只关心打赏成功了没
+// 不要提前定义字段,直到有需要
+type GetRewardResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Status RewardStatus `protobuf:"varint,1,opt,name=status,proto3,enum=reward.v1.RewardStatus" json:"status,omitempty"`
+}
+
+func (x *GetRewardResponse) Reset() {
+ *x = GetRewardResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_reward_v1_reward_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetRewardResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetRewardResponse) ProtoMessage() {}
+
+func (x *GetRewardResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_reward_v1_reward_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetRewardResponse.ProtoReflect.Descriptor instead.
+func (*GetRewardResponse) Descriptor() ([]byte, []int) {
+ return file_reward_v1_reward_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetRewardResponse) GetStatus() RewardStatus {
+ if x != nil {
+ return x.Status
+ }
+ return RewardStatus_RewardStatusUnknown
+}
+
+type PreRewardRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ // 用户能够理解的,它打赏的是什么东西
+ BizName string `protobuf:"bytes,3,opt,name=biz_name,json=bizName,proto3" json:"biz_name,omitempty"`
+ // 被打赏的人,也就是收钱的那个
+ TargetUid int64 `protobuf:"varint,4,opt,name=target_uid,json=targetUid,proto3" json:"target_uid,omitempty"`
+ // 打赏的人,也就是付钱的那个
+ Uid int64 `protobuf:"varint,5,opt,name=uid,proto3" json:"uid,omitempty"`
+ // 打赏金额
+ Amt int64 `protobuf:"varint,6,opt,name=amt,proto3" json:"amt,omitempty"` // 这里要不要货币?
+}
+
+func (x *PreRewardRequest) Reset() {
+ *x = PreRewardRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_reward_v1_reward_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PreRewardRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PreRewardRequest) ProtoMessage() {}
+
+func (x *PreRewardRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_reward_v1_reward_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PreRewardRequest.ProtoReflect.Descriptor instead.
+func (*PreRewardRequest) Descriptor() ([]byte, []int) {
+ return file_reward_v1_reward_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *PreRewardRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *PreRewardRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *PreRewardRequest) GetBizName() string {
+ if x != nil {
+ return x.BizName
+ }
+ return ""
+}
+
+func (x *PreRewardRequest) GetTargetUid() int64 {
+ if x != nil {
+ return x.TargetUid
+ }
+ return 0
+}
+
+func (x *PreRewardRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *PreRewardRequest) GetAmt() int64 {
+ if x != nil {
+ return x.Amt
+ }
+ return 0
+}
+
+type PreRewardResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 打赏这个东西,不存在说后面换支付啥的,
+ //
+ // 或者说至少现在没有啥必要考虑
+ // 所以直接耦合了微信扫码支付的 code_url 的说法
+ CodeUrl string `protobuf:"bytes,1,opt,name=code_url,json=codeUrl,proto3" json:"code_url,omitempty"`
+ // 打赏的 ID
+ Rid int64 `protobuf:"varint,2,opt,name=rid,proto3" json:"rid,omitempty"`
+}
+
+func (x *PreRewardResponse) Reset() {
+ *x = PreRewardResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_reward_v1_reward_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PreRewardResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PreRewardResponse) ProtoMessage() {}
+
+func (x *PreRewardResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_reward_v1_reward_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PreRewardResponse.ProtoReflect.Descriptor instead.
+func (*PreRewardResponse) Descriptor() ([]byte, []int) {
+ return file_reward_v1_reward_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *PreRewardResponse) GetCodeUrl() string {
+ if x != nil {
+ return x.CodeUrl
+ }
+ return ""
+}
+
+func (x *PreRewardResponse) GetRid() int64 {
+ if x != nil {
+ return x.Rid
+ }
+ return 0
+}
+
+var File_reward_v1_reward_proto protoreflect.FileDescriptor
+
+var file_reward_v1_reward_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x77, 0x61,
+ 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
+ 0x2e, 0x76, 0x31, 0x22, 0x36, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x72, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x11, 0x47,
+ 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
+ 0x32, 0x17, 0x2e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x77,
+ 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x22, 0x99, 0x01, 0x0a, 0x10, 0x50, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15, 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f,
+ 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12,
+ 0x19, 0x0a, 0x08, 0x62, 0x69, 0x7a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x07, 0x62, 0x69, 0x7a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x61,
+ 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
+ 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64,
+ 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61,
+ 0x6d, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x22, 0x40, 0x0a,
+ 0x11, 0x50, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x64, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x10, 0x0a,
+ 0x03, 0x72, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x72, 0x69, 0x64, 0x2a,
+ 0x6c, 0x0a, 0x0c, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+ 0x17, 0x0a, 0x13, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
+ 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x65, 0x77, 0x61,
+ 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x10, 0x01, 0x12, 0x15,
+ 0x0a, 0x11, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x50, 0x61,
+ 0x79, 0x65, 0x64, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53,
+ 0x74, 0x61, 0x74, 0x75, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x03, 0x32, 0x9f, 0x01,
+ 0x0a, 0x0d, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
+ 0x46, 0x0a, 0x09, 0x50, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x12, 0x1b, 0x2e, 0x72,
+ 0x65, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61,
+ 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x72, 0x65, 0x77, 0x61,
+ 0x72, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x65,
+ 0x77, 0x61, 0x72, 0x64, 0x12, 0x1b, 0x2e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x76, 0x31,
+ 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x1c, 0x2e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
+ 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+ 0xa6, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x76,
+ 0x31, 0x42, 0x0b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
+ 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b,
+ 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65,
+ 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67,
+ 0x65, 0x6e, 0x2f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x65, 0x77,
+ 0x61, 0x72, 0x64, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x52, 0x65,
+ 0x77, 0x61, 0x72, 0x64, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x09, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
+ 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5c, 0x56, 0x31, 0x5c,
+ 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x52, 0x65,
+ 0x77, 0x61, 0x72, 0x64, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_reward_v1_reward_proto_rawDescOnce sync.Once
+ file_reward_v1_reward_proto_rawDescData = file_reward_v1_reward_proto_rawDesc
+)
+
+func file_reward_v1_reward_proto_rawDescGZIP() []byte {
+ file_reward_v1_reward_proto_rawDescOnce.Do(func() {
+ file_reward_v1_reward_proto_rawDescData = protoimpl.X.CompressGZIP(file_reward_v1_reward_proto_rawDescData)
+ })
+ return file_reward_v1_reward_proto_rawDescData
+}
+
+var file_reward_v1_reward_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_reward_v1_reward_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_reward_v1_reward_proto_goTypes = []interface{}{
+ (RewardStatus)(0), // 0: reward.v1.RewardStatus
+ (*GetRewardRequest)(nil), // 1: reward.v1.GetRewardRequest
+ (*GetRewardResponse)(nil), // 2: reward.v1.GetRewardResponse
+ (*PreRewardRequest)(nil), // 3: reward.v1.PreRewardRequest
+ (*PreRewardResponse)(nil), // 4: reward.v1.PreRewardResponse
+}
+var file_reward_v1_reward_proto_depIdxs = []int32{
+ 0, // 0: reward.v1.GetRewardResponse.status:type_name -> reward.v1.RewardStatus
+ 3, // 1: reward.v1.RewardService.PreReward:input_type -> reward.v1.PreRewardRequest
+ 1, // 2: reward.v1.RewardService.GetReward:input_type -> reward.v1.GetRewardRequest
+ 4, // 3: reward.v1.RewardService.PreReward:output_type -> reward.v1.PreRewardResponse
+ 2, // 4: reward.v1.RewardService.GetReward:output_type -> reward.v1.GetRewardResponse
+ 3, // [3:5] is the sub-list for method output_type
+ 1, // [1:3] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_reward_v1_reward_proto_init() }
+func file_reward_v1_reward_proto_init() {
+ if File_reward_v1_reward_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_reward_v1_reward_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetRewardRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_reward_v1_reward_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetRewardResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_reward_v1_reward_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PreRewardRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_reward_v1_reward_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PreRewardResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_reward_v1_reward_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_reward_v1_reward_proto_goTypes,
+ DependencyIndexes: file_reward_v1_reward_proto_depIdxs,
+ EnumInfos: file_reward_v1_reward_proto_enumTypes,
+ MessageInfos: file_reward_v1_reward_proto_msgTypes,
+ }.Build()
+ File_reward_v1_reward_proto = out.File
+ file_reward_v1_reward_proto_rawDesc = nil
+ file_reward_v1_reward_proto_goTypes = nil
+ file_reward_v1_reward_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/reward/v1/reward_grpc.pb.go b/webook/api/proto/gen/reward/v1/reward_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..51804942dcf88f41bf1f4c0dccd4572ee9a0b2f9
--- /dev/null
+++ b/webook/api/proto/gen/reward/v1/reward_grpc.pb.go
@@ -0,0 +1,146 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: reward/v1/reward.proto
+
+package rewardv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ RewardService_PreReward_FullMethodName = "/reward.v1.RewardService/PreReward"
+ RewardService_GetReward_FullMethodName = "/reward.v1.RewardService/GetReward"
+)
+
+// RewardServiceClient is the client API for RewardService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type RewardServiceClient interface {
+ PreReward(ctx context.Context, in *PreRewardRequest, opts ...grpc.CallOption) (*PreRewardResponse, error)
+ GetReward(ctx context.Context, in *GetRewardRequest, opts ...grpc.CallOption) (*GetRewardResponse, error)
+}
+
+type rewardServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewRewardServiceClient(cc grpc.ClientConnInterface) RewardServiceClient {
+ return &rewardServiceClient{cc}
+}
+
+func (c *rewardServiceClient) PreReward(ctx context.Context, in *PreRewardRequest, opts ...grpc.CallOption) (*PreRewardResponse, error) {
+ out := new(PreRewardResponse)
+ err := c.cc.Invoke(ctx, RewardService_PreReward_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *rewardServiceClient) GetReward(ctx context.Context, in *GetRewardRequest, opts ...grpc.CallOption) (*GetRewardResponse, error) {
+ out := new(GetRewardResponse)
+ err := c.cc.Invoke(ctx, RewardService_GetReward_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// RewardServiceServer is the server API for RewardService service.
+// All implementations must embed UnimplementedRewardServiceServer
+// for forward compatibility
+type RewardServiceServer interface {
+ PreReward(context.Context, *PreRewardRequest) (*PreRewardResponse, error)
+ GetReward(context.Context, *GetRewardRequest) (*GetRewardResponse, error)
+ mustEmbedUnimplementedRewardServiceServer()
+}
+
+// UnimplementedRewardServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedRewardServiceServer struct {
+}
+
+func (UnimplementedRewardServiceServer) PreReward(context.Context, *PreRewardRequest) (*PreRewardResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method PreReward not implemented")
+}
+func (UnimplementedRewardServiceServer) GetReward(context.Context, *GetRewardRequest) (*GetRewardResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetReward not implemented")
+}
+func (UnimplementedRewardServiceServer) mustEmbedUnimplementedRewardServiceServer() {}
+
+// UnsafeRewardServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to RewardServiceServer will
+// result in compilation errors.
+type UnsafeRewardServiceServer interface {
+ mustEmbedUnimplementedRewardServiceServer()
+}
+
+func RegisterRewardServiceServer(s grpc.ServiceRegistrar, srv RewardServiceServer) {
+ s.RegisterService(&RewardService_ServiceDesc, srv)
+}
+
+func _RewardService_PreReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(PreRewardRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RewardServiceServer).PreReward(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: RewardService_PreReward_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RewardServiceServer).PreReward(ctx, req.(*PreRewardRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _RewardService_GetReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetRewardRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RewardServiceServer).GetReward(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: RewardService_GetReward_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RewardServiceServer).GetReward(ctx, req.(*GetRewardRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// RewardService_ServiceDesc is the grpc.ServiceDesc for RewardService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var RewardService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "reward.v1.RewardService",
+ HandlerType: (*RewardServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "PreReward",
+ Handler: _RewardService_PreReward_Handler,
+ },
+ {
+ MethodName: "GetReward",
+ Handler: _RewardService_GetReward_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "reward/v1/reward.proto",
+}
diff --git a/webook/api/proto/gen/search/v1/search.pb.go b/webook/api/proto/gen/search/v1/search.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..07254f29baf3d0f3fde26fc219c985c66966e4ff
--- /dev/null
+++ b/webook/api/proto/gen/search/v1/search.pb.go
@@ -0,0 +1,382 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: search/v1/search.proto
+
+package searchv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SearchRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Expression string `protobuf:"bytes,1,opt,name=expression,proto3" json:"expression,omitempty"`
+ Uid int64 `protobuf:"varint,2,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *SearchRequest) Reset() {
+ *x = SearchRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_search_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SearchRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SearchRequest) ProtoMessage() {}
+
+func (x *SearchRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_search_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead.
+func (*SearchRequest) Descriptor() ([]byte, []int) {
+ return file_search_v1_search_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SearchRequest) GetExpression() string {
+ if x != nil {
+ return x.Expression
+ }
+ return ""
+}
+
+func (x *SearchRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type SearchResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 分类展示数据
+ User *UserResult `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+ Article *ArticleResult `protobuf:"bytes,2,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *SearchResponse) Reset() {
+ *x = SearchResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_search_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SearchResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SearchResponse) ProtoMessage() {}
+
+func (x *SearchResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_search_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead.
+func (*SearchResponse) Descriptor() ([]byte, []int) {
+ return file_search_v1_search_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *SearchResponse) GetUser() *UserResult {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+func (x *SearchResponse) GetArticle() *ArticleResult {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type UserResult struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
+}
+
+func (x *UserResult) Reset() {
+ *x = UserResult{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_search_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UserResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UserResult) ProtoMessage() {}
+
+func (x *UserResult) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_search_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UserResult.ProtoReflect.Descriptor instead.
+func (*UserResult) Descriptor() ([]byte, []int) {
+ return file_search_v1_search_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *UserResult) GetUsers() []*User {
+ if x != nil {
+ return x.Users
+ }
+ return nil
+}
+
+type ArticleResult struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Articles []*Article `protobuf:"bytes,1,rep,name=articles,proto3" json:"articles,omitempty"`
+}
+
+func (x *ArticleResult) Reset() {
+ *x = ArticleResult{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_search_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ArticleResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ArticleResult) ProtoMessage() {}
+
+func (x *ArticleResult) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_search_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ArticleResult.ProtoReflect.Descriptor instead.
+func (*ArticleResult) Descriptor() ([]byte, []int) {
+ return file_search_v1_search_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ArticleResult) GetArticles() []*Article {
+ if x != nil {
+ return x.Articles
+ }
+ return nil
+}
+
+var File_search_v1_search_proto protoreflect.FileDescriptor
+
+var file_search_v1_search_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x61, 0x72,
+ 0x63, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
+ 0x2e, 0x76, 0x31, 0x1a, 0x14, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x73,
+ 0x79, 0x6e, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x41, 0x0a, 0x0d, 0x53, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78,
+ 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
+ 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69,
+ 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x6f, 0x0a, 0x0e,
+ 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29,
+ 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x73,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73,
+ 0x75, 0x6c, 0x74, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x65,
+ 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x22, 0x33, 0x0a,
+ 0x0a, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x25, 0x0a, 0x05, 0x75,
+ 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65,
+ 0x72, 0x73, 0x22, 0x3f, 0x0a, 0x0d, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x73,
+ 0x75, 0x6c, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x73, 0x18,
+ 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
+ 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x63,
+ 0x6c, 0x65, 0x73, 0x32, 0x4e, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x53, 0x65, 0x72,
+ 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x18,
+ 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x32, 0x14, 0x0a, 0x12, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0xa6, 0x01, 0x0a, 0x0d, 0x63, 0x6f,
+ 0x6d, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x53, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65,
+ 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62,
+ 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61,
+ 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x76, 0x31, 0xa2,
+ 0x02, 0x03, 0x53, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x56,
+ 0x31, 0xca, 0x02, 0x09, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15,
+ 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x3a, 0x3a,
+ 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_search_v1_search_proto_rawDescOnce sync.Once
+ file_search_v1_search_proto_rawDescData = file_search_v1_search_proto_rawDesc
+)
+
+func file_search_v1_search_proto_rawDescGZIP() []byte {
+ file_search_v1_search_proto_rawDescOnce.Do(func() {
+ file_search_v1_search_proto_rawDescData = protoimpl.X.CompressGZIP(file_search_v1_search_proto_rawDescData)
+ })
+ return file_search_v1_search_proto_rawDescData
+}
+
+var file_search_v1_search_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_search_v1_search_proto_goTypes = []interface{}{
+ (*SearchRequest)(nil), // 0: search.v1.SearchRequest
+ (*SearchResponse)(nil), // 1: search.v1.SearchResponse
+ (*UserResult)(nil), // 2: search.v1.UserResult
+ (*ArticleResult)(nil), // 3: search.v1.ArticleResult
+ (*User)(nil), // 4: search.v1.User
+ (*Article)(nil), // 5: search.v1.Article
+}
+var file_search_v1_search_proto_depIdxs = []int32{
+ 2, // 0: search.v1.SearchResponse.user:type_name -> search.v1.UserResult
+ 3, // 1: search.v1.SearchResponse.article:type_name -> search.v1.ArticleResult
+ 4, // 2: search.v1.UserResult.users:type_name -> search.v1.User
+ 5, // 3: search.v1.ArticleResult.articles:type_name -> search.v1.Article
+ 0, // 4: search.v1.SearchService.Search:input_type -> search.v1.SearchRequest
+ 1, // 5: search.v1.SearchService.Search:output_type -> search.v1.SearchResponse
+ 5, // [5:6] is the sub-list for method output_type
+ 4, // [4:5] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_search_v1_search_proto_init() }
+func file_search_v1_search_proto_init() {
+ if File_search_v1_search_proto != nil {
+ return
+ }
+ file_search_v1_sync_proto_init()
+ if !protoimpl.UnsafeEnabled {
+ file_search_v1_search_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SearchRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_search_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SearchResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UserResult); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_search_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ArticleResult); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_search_v1_search_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 2,
+ },
+ GoTypes: file_search_v1_search_proto_goTypes,
+ DependencyIndexes: file_search_v1_search_proto_depIdxs,
+ MessageInfos: file_search_v1_search_proto_msgTypes,
+ }.Build()
+ File_search_v1_search_proto = out.File
+ file_search_v1_search_proto_rawDesc = nil
+ file_search_v1_search_proto_goTypes = nil
+ file_search_v1_search_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/search/v1/search_grpc.pb.go b/webook/api/proto/gen/search/v1/search_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..f7d9973f186e0db508fc3fa779648f6018956774
--- /dev/null
+++ b/webook/api/proto/gen/search/v1/search_grpc.pb.go
@@ -0,0 +1,162 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: search/v1/search.proto
+
+package searchv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ SearchService_Search_FullMethodName = "/search.v1.SearchService/Search"
+)
+
+// SearchServiceClient is the client API for SearchService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type SearchServiceClient interface {
+ // 这个是最为模糊的搜索接口
+ Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
+}
+
+type searchServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewSearchServiceClient(cc grpc.ClientConnInterface) SearchServiceClient {
+ return &searchServiceClient{cc}
+}
+
+func (c *searchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {
+ out := new(SearchResponse)
+ err := c.cc.Invoke(ctx, SearchService_Search_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// SearchServiceServer is the server API for SearchService service.
+// All implementations must embed UnimplementedSearchServiceServer
+// for forward compatibility
+type SearchServiceServer interface {
+ // 这个是最为模糊的搜索接口
+ Search(context.Context, *SearchRequest) (*SearchResponse, error)
+ mustEmbedUnimplementedSearchServiceServer()
+}
+
+// UnimplementedSearchServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedSearchServiceServer struct {
+}
+
+func (UnimplementedSearchServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Search not implemented")
+}
+func (UnimplementedSearchServiceServer) mustEmbedUnimplementedSearchServiceServer() {}
+
+// UnsafeSearchServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to SearchServiceServer will
+// result in compilation errors.
+type UnsafeSearchServiceServer interface {
+ mustEmbedUnimplementedSearchServiceServer()
+}
+
+func RegisterSearchServiceServer(s grpc.ServiceRegistrar, srv SearchServiceServer) {
+ s.RegisterService(&SearchService_ServiceDesc, srv)
+}
+
+func _SearchService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SearchRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SearchServiceServer).Search(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: SearchService_Search_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SearchServiceServer).Search(ctx, req.(*SearchRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// SearchService_ServiceDesc is the grpc.ServiceDesc for SearchService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var SearchService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "search.v1.SearchService",
+ HandlerType: (*SearchServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Search",
+ Handler: _SearchService_Search_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "search/v1/search.proto",
+}
+
+const ()
+
+// UserServiceServiceClient is the client API for UserServiceService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type UserServiceServiceClient interface {
+}
+
+type userServiceServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewUserServiceServiceClient(cc grpc.ClientConnInterface) UserServiceServiceClient {
+ return &userServiceServiceClient{cc}
+}
+
+// UserServiceServiceServer is the server API for UserServiceService service.
+// All implementations must embed UnimplementedUserServiceServiceServer
+// for forward compatibility
+type UserServiceServiceServer interface {
+ mustEmbedUnimplementedUserServiceServiceServer()
+}
+
+// UnimplementedUserServiceServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedUserServiceServiceServer struct {
+}
+
+func (UnimplementedUserServiceServiceServer) mustEmbedUnimplementedUserServiceServiceServer() {}
+
+// UnsafeUserServiceServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to UserServiceServiceServer will
+// result in compilation errors.
+type UnsafeUserServiceServiceServer interface {
+ mustEmbedUnimplementedUserServiceServiceServer()
+}
+
+func RegisterUserServiceServiceServer(s grpc.ServiceRegistrar, srv UserServiceServiceServer) {
+ s.RegisterService(&UserServiceService_ServiceDesc, srv)
+}
+
+// UserServiceService_ServiceDesc is the grpc.ServiceDesc for UserServiceService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var UserServiceService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "search.v1.UserServiceService",
+ HandlerType: (*UserServiceServiceServer)(nil),
+ Methods: []grpc.MethodDesc{},
+ Streams: []grpc.StreamDesc{},
+ Metadata: "search/v1/search.proto",
+}
diff --git a/webook/api/proto/gen/search/v1/sync.pb.go b/webook/api/proto/gen/search/v1/sync.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..2f2d70c465afb8233111da85cbb018c32978e4d5
--- /dev/null
+++ b/webook/api/proto/gen/search/v1/sync.pb.go
@@ -0,0 +1,669 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: search/v1/sync.proto
+
+package searchv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type InputAnyRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ IndexName string `protobuf:"bytes,1,opt,name=index_name,json=indexName,proto3" json:"index_name,omitempty"`
+ DocId string `protobuf:"bytes,2,opt,name=doc_id,json=docId,proto3" json:"doc_id,omitempty"`
+ Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *InputAnyRequest) Reset() {
+ *x = InputAnyRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputAnyRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputAnyRequest) ProtoMessage() {}
+
+func (x *InputAnyRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputAnyRequest.ProtoReflect.Descriptor instead.
+func (*InputAnyRequest) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *InputAnyRequest) GetIndexName() string {
+ if x != nil {
+ return x.IndexName
+ }
+ return ""
+}
+
+func (x *InputAnyRequest) GetDocId() string {
+ if x != nil {
+ return x.DocId
+ }
+ return ""
+}
+
+func (x *InputAnyRequest) GetData() string {
+ if x != nil {
+ return x.Data
+ }
+ return ""
+}
+
+type InputAnyResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *InputAnyResponse) Reset() {
+ *x = InputAnyResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputAnyResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputAnyResponse) ProtoMessage() {}
+
+func (x *InputAnyResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputAnyResponse.ProtoReflect.Descriptor instead.
+func (*InputAnyResponse) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{1}
+}
+
+type InputUserRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *InputUserRequest) Reset() {
+ *x = InputUserRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputUserRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputUserRequest) ProtoMessage() {}
+
+func (x *InputUserRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputUserRequest.ProtoReflect.Descriptor instead.
+func (*InputUserRequest) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *InputUserRequest) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type InputUserResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *InputUserResponse) Reset() {
+ *x = InputUserResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputUserResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputUserResponse) ProtoMessage() {}
+
+func (x *InputUserResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputUserResponse.ProtoReflect.Descriptor instead.
+func (*InputUserResponse) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{3}
+}
+
+type InputArticleRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Article *Article `protobuf:"bytes,1,opt,name=article,proto3" json:"article,omitempty"`
+}
+
+func (x *InputArticleRequest) Reset() {
+ *x = InputArticleRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputArticleRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputArticleRequest) ProtoMessage() {}
+
+func (x *InputArticleRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputArticleRequest.ProtoReflect.Descriptor instead.
+func (*InputArticleRequest) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *InputArticleRequest) GetArticle() *Article {
+ if x != nil {
+ return x.Article
+ }
+ return nil
+}
+
+type InputArticleResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *InputArticleResponse) Reset() {
+ *x = InputArticleResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *InputArticleResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InputArticleResponse) ProtoMessage() {}
+
+func (x *InputArticleResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InputArticleResponse.ProtoReflect.Descriptor instead.
+func (*InputArticleResponse) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{5}
+}
+
+type Article struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+ Status int32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"`
+ Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+ Tags []string `protobuf:"bytes,5,rep,name=tags,proto3" json:"tags,omitempty"`
+}
+
+func (x *Article) Reset() {
+ *x = Article{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Article) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *Article) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Article) GetTitle() string {
+ if x != nil {
+ return x.Title
+ }
+ return ""
+}
+
+func (x *Article) GetStatus() int32 {
+ if x != nil {
+ return x.Status
+ }
+ return 0
+}
+
+func (x *Article) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *Article) GetTags() []string {
+ if x != nil {
+ return x.Tags
+ }
+ return nil
+}
+
+type User struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
+ Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"`
+ Phone string `protobuf:"bytes,4,opt,name=phone,proto3" json:"phone,omitempty"`
+}
+
+func (x *User) Reset() {
+ *x = User{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_search_v1_sync_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *User) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*User) ProtoMessage() {}
+
+func (x *User) ProtoReflect() protoreflect.Message {
+ mi := &file_search_v1_sync_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use User.ProtoReflect.Descriptor instead.
+func (*User) Descriptor() ([]byte, []int) {
+ return file_search_v1_sync_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *User) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *User) GetEmail() string {
+ if x != nil {
+ return x.Email
+ }
+ return ""
+}
+
+func (x *User) GetNickname() string {
+ if x != nil {
+ return x.Nickname
+ }
+ return ""
+}
+
+func (x *User) GetPhone() string {
+ if x != nil {
+ return x.Phone
+ }
+ return ""
+}
+
+var File_search_v1_sync_proto protoreflect.FileDescriptor
+
+var file_search_v1_sync_proto_rawDesc = []byte{
+ 0x0a, 0x14, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x79, 0x6e, 0x63,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
+ 0x31, 0x22, 0x5b, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x6e, 0x79, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6e, 0x61,
+ 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x4e,
+ 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x6f, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x64, 0x6f, 0x63, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61,
+ 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x12,
+ 0x0a, 0x10, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x6e, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x22, 0x37, 0x0a, 0x10, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31,
+ 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x13, 0x0a, 0x11, 0x49,
+ 0x6e, 0x70, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x22, 0x43, 0x0a, 0x13, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x07, 0x61, 0x72, 0x74, 0x69, 0x63,
+ 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x07, 0x61, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x75, 0x0a,
+ 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16,
+ 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06,
+ 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+ 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
+ 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04,
+ 0x74, 0x61, 0x67, 0x73, 0x22, 0x5e, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02,
+ 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05,
+ 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61,
+ 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14,
+ 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70,
+ 0x68, 0x6f, 0x6e, 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0b, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x65, 0x72,
+ 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x55, 0x73, 0x65,
+ 0x72, 0x12, 0x1b, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e,
+ 0x70, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c,
+ 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74,
+ 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c,
+ 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x12, 0x1e, 0x2e, 0x73,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x73,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72,
+ 0x74, 0x69, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a,
+ 0x08, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x6e, 0x79, 0x12, 0x1a, 0x2e, 0x73, 0x65, 0x61, 0x72,
+ 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x6e, 0x79, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
+ 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x6e, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x42, 0xa4, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x53, 0x79, 0x6e, 0x63, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
+ 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65,
+ 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77,
+ 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+ 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x65,
+ 0x61, 0x72, 0x63, 0x68, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x53, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x53,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x09, 0x53, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x15, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5c, 0x56, 0x31,
+ 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x53,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
+}
+
+var (
+ file_search_v1_sync_proto_rawDescOnce sync.Once
+ file_search_v1_sync_proto_rawDescData = file_search_v1_sync_proto_rawDesc
+)
+
+func file_search_v1_sync_proto_rawDescGZIP() []byte {
+ file_search_v1_sync_proto_rawDescOnce.Do(func() {
+ file_search_v1_sync_proto_rawDescData = protoimpl.X.CompressGZIP(file_search_v1_sync_proto_rawDescData)
+ })
+ return file_search_v1_sync_proto_rawDescData
+}
+
+var file_search_v1_sync_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_search_v1_sync_proto_goTypes = []interface{}{
+ (*InputAnyRequest)(nil), // 0: search.v1.InputAnyRequest
+ (*InputAnyResponse)(nil), // 1: search.v1.InputAnyResponse
+ (*InputUserRequest)(nil), // 2: search.v1.InputUserRequest
+ (*InputUserResponse)(nil), // 3: search.v1.InputUserResponse
+ (*InputArticleRequest)(nil), // 4: search.v1.InputArticleRequest
+ (*InputArticleResponse)(nil), // 5: search.v1.InputArticleResponse
+ (*Article)(nil), // 6: search.v1.Article
+ (*User)(nil), // 7: search.v1.User
+}
+var file_search_v1_sync_proto_depIdxs = []int32{
+ 7, // 0: search.v1.InputUserRequest.user:type_name -> search.v1.User
+ 6, // 1: search.v1.InputArticleRequest.article:type_name -> search.v1.Article
+ 2, // 2: search.v1.SyncService.InputUser:input_type -> search.v1.InputUserRequest
+ 4, // 3: search.v1.SyncService.InputArticle:input_type -> search.v1.InputArticleRequest
+ 0, // 4: search.v1.SyncService.InputAny:input_type -> search.v1.InputAnyRequest
+ 3, // 5: search.v1.SyncService.InputUser:output_type -> search.v1.InputUserResponse
+ 5, // 6: search.v1.SyncService.InputArticle:output_type -> search.v1.InputArticleResponse
+ 1, // 7: search.v1.SyncService.InputAny:output_type -> search.v1.InputAnyResponse
+ 5, // [5:8] is the sub-list for method output_type
+ 2, // [2:5] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_search_v1_sync_proto_init() }
+func file_search_v1_sync_proto_init() {
+ if File_search_v1_sync_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_search_v1_sync_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputAnyRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputAnyResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputUserRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputUserResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputArticleRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*InputArticleResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Article); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_search_v1_sync_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*User); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_search_v1_sync_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 8,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_search_v1_sync_proto_goTypes,
+ DependencyIndexes: file_search_v1_sync_proto_depIdxs,
+ MessageInfos: file_search_v1_sync_proto_msgTypes,
+ }.Build()
+ File_search_v1_sync_proto = out.File
+ file_search_v1_sync_proto_rawDesc = nil
+ file_search_v1_sync_proto_goTypes = nil
+ file_search_v1_sync_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/search/v1/sync_grpc.pb.go b/webook/api/proto/gen/search/v1/sync_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..945769ceb13e10c65031943284a2ccd7b974507b
--- /dev/null
+++ b/webook/api/proto/gen/search/v1/sync_grpc.pb.go
@@ -0,0 +1,189 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: search/v1/sync.proto
+
+package searchv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ SyncService_InputUser_FullMethodName = "/search.v1.SyncService/InputUser"
+ SyncService_InputArticle_FullMethodName = "/search.v1.SyncService/InputArticle"
+ SyncService_InputAny_FullMethodName = "/search.v1.SyncService/InputAny"
+)
+
+// SyncServiceClient is the client API for SyncService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type SyncServiceClient interface {
+ InputUser(ctx context.Context, in *InputUserRequest, opts ...grpc.CallOption) (*InputUserResponse, error)
+ InputArticle(ctx context.Context, in *InputArticleRequest, opts ...grpc.CallOption) (*InputArticleResponse, error)
+ // 假如说我没有这个功能
+ // 能用,但是不好用,或者说不能提供业务定制化功能
+ // 兜底
+ InputAny(ctx context.Context, in *InputAnyRequest, opts ...grpc.CallOption) (*InputAnyResponse, error)
+}
+
+type syncServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewSyncServiceClient(cc grpc.ClientConnInterface) SyncServiceClient {
+ return &syncServiceClient{cc}
+}
+
+func (c *syncServiceClient) InputUser(ctx context.Context, in *InputUserRequest, opts ...grpc.CallOption) (*InputUserResponse, error) {
+ out := new(InputUserResponse)
+ err := c.cc.Invoke(ctx, SyncService_InputUser_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *syncServiceClient) InputArticle(ctx context.Context, in *InputArticleRequest, opts ...grpc.CallOption) (*InputArticleResponse, error) {
+ out := new(InputArticleResponse)
+ err := c.cc.Invoke(ctx, SyncService_InputArticle_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *syncServiceClient) InputAny(ctx context.Context, in *InputAnyRequest, opts ...grpc.CallOption) (*InputAnyResponse, error) {
+ out := new(InputAnyResponse)
+ err := c.cc.Invoke(ctx, SyncService_InputAny_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// SyncServiceServer is the server API for SyncService service.
+// All implementations must embed UnimplementedSyncServiceServer
+// for forward compatibility
+type SyncServiceServer interface {
+ InputUser(context.Context, *InputUserRequest) (*InputUserResponse, error)
+ InputArticle(context.Context, *InputArticleRequest) (*InputArticleResponse, error)
+ // 假如说我没有这个功能
+ // 能用,但是不好用,或者说不能提供业务定制化功能
+ // 兜底
+ InputAny(context.Context, *InputAnyRequest) (*InputAnyResponse, error)
+ mustEmbedUnimplementedSyncServiceServer()
+}
+
+// UnimplementedSyncServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedSyncServiceServer struct {
+}
+
+func (UnimplementedSyncServiceServer) InputUser(context.Context, *InputUserRequest) (*InputUserResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method InputUser not implemented")
+}
+func (UnimplementedSyncServiceServer) InputArticle(context.Context, *InputArticleRequest) (*InputArticleResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method InputArticle not implemented")
+}
+func (UnimplementedSyncServiceServer) InputAny(context.Context, *InputAnyRequest) (*InputAnyResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method InputAny not implemented")
+}
+func (UnimplementedSyncServiceServer) mustEmbedUnimplementedSyncServiceServer() {}
+
+// UnsafeSyncServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to SyncServiceServer will
+// result in compilation errors.
+type UnsafeSyncServiceServer interface {
+ mustEmbedUnimplementedSyncServiceServer()
+}
+
+func RegisterSyncServiceServer(s grpc.ServiceRegistrar, srv SyncServiceServer) {
+ s.RegisterService(&SyncService_ServiceDesc, srv)
+}
+
+func _SyncService_InputUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(InputUserRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SyncServiceServer).InputUser(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: SyncService_InputUser_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SyncServiceServer).InputUser(ctx, req.(*InputUserRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _SyncService_InputArticle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(InputArticleRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SyncServiceServer).InputArticle(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: SyncService_InputArticle_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SyncServiceServer).InputArticle(ctx, req.(*InputArticleRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _SyncService_InputAny_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(InputAnyRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SyncServiceServer).InputAny(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: SyncService_InputAny_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SyncServiceServer).InputAny(ctx, req.(*InputAnyRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// SyncService_ServiceDesc is the grpc.ServiceDesc for SyncService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var SyncService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "search.v1.SyncService",
+ HandlerType: (*SyncServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "InputUser",
+ Handler: _SyncService_InputUser_Handler,
+ },
+ {
+ MethodName: "InputArticle",
+ Handler: _SyncService_InputArticle_Handler,
+ },
+ {
+ MethodName: "InputAny",
+ Handler: _SyncService_InputAny_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "search/v1/sync.proto",
+}
diff --git a/webook/api/proto/gen/sms/v1/sms.pb.go b/webook/api/proto/gen/sms/v1/sms.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..cb05b0ef962037b4eaa14a8030650eafcb5235ee
--- /dev/null
+++ b/webook/api/proto/gen/sms/v1/sms.pb.go
@@ -0,0 +1,228 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: sms/v1/sms.proto
+
+package smsv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SmsSendRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ TplId string `protobuf:"bytes,1,opt,name=tplId,proto3" json:"tplId,omitempty"`
+ Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
+ Numbers []string `protobuf:"bytes,3,rep,name=numbers,proto3" json:"numbers,omitempty"`
+}
+
+func (x *SmsSendRequest) Reset() {
+ *x = SmsSendRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_sms_v1_sms_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SmsSendRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SmsSendRequest) ProtoMessage() {}
+
+func (x *SmsSendRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_sms_v1_sms_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SmsSendRequest.ProtoReflect.Descriptor instead.
+func (*SmsSendRequest) Descriptor() ([]byte, []int) {
+ return file_sms_v1_sms_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SmsSendRequest) GetTplId() string {
+ if x != nil {
+ return x.TplId
+ }
+ return ""
+}
+
+func (x *SmsSendRequest) GetArgs() []string {
+ if x != nil {
+ return x.Args
+ }
+ return nil
+}
+
+func (x *SmsSendRequest) GetNumbers() []string {
+ if x != nil {
+ return x.Numbers
+ }
+ return nil
+}
+
+type SmsSendResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *SmsSendResponse) Reset() {
+ *x = SmsSendResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_sms_v1_sms_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SmsSendResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SmsSendResponse) ProtoMessage() {}
+
+func (x *SmsSendResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_sms_v1_sms_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SmsSendResponse.ProtoReflect.Descriptor instead.
+func (*SmsSendResponse) Descriptor() ([]byte, []int) {
+ return file_sms_v1_sms_proto_rawDescGZIP(), []int{1}
+}
+
+var File_sms_v1_sms_proto protoreflect.FileDescriptor
+
+var file_sms_v1_sms_proto_rawDesc = []byte{
+ 0x0a, 0x10, 0x73, 0x6d, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6d, 0x73, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x12, 0x06, 0x73, 0x6d, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x54, 0x0a, 0x0e, 0x53, 0x6d,
+ 0x73, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
+ 0x74, 0x70, 0x6c, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x70, 0x6c,
+ 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
+ 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+ 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73,
+ 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x6d, 0x73, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x32, 0x45, 0x0a, 0x0a, 0x53, 0x6d, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x37, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x16, 0x2e, 0x73, 0x6d, 0x73, 0x2e,
+ 0x76, 0x31, 0x2e, 0x53, 0x6d, 0x73, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x17, 0x2e, 0x73, 0x6d, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6d, 0x73, 0x53, 0x65,
+ 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x8e, 0x01, 0x0a, 0x0a, 0x63,
+ 0x6f, 0x6d, 0x2e, 0x73, 0x6d, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x08, 0x53, 0x6d, 0x73, 0x50, 0x72,
+ 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d,
+ 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x6d, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73,
+ 0x6d, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x53, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x53, 0x6d, 0x73,
+ 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x53, 0x6d, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, 0x53,
+ 0x6d, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+ 0x61, 0xea, 0x02, 0x07, 0x53, 0x6d, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_sms_v1_sms_proto_rawDescOnce sync.Once
+ file_sms_v1_sms_proto_rawDescData = file_sms_v1_sms_proto_rawDesc
+)
+
+func file_sms_v1_sms_proto_rawDescGZIP() []byte {
+ file_sms_v1_sms_proto_rawDescOnce.Do(func() {
+ file_sms_v1_sms_proto_rawDescData = protoimpl.X.CompressGZIP(file_sms_v1_sms_proto_rawDescData)
+ })
+ return file_sms_v1_sms_proto_rawDescData
+}
+
+var file_sms_v1_sms_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_sms_v1_sms_proto_goTypes = []interface{}{
+ (*SmsSendRequest)(nil), // 0: sms.v1.SmsSendRequest
+ (*SmsSendResponse)(nil), // 1: sms.v1.SmsSendResponse
+}
+var file_sms_v1_sms_proto_depIdxs = []int32{
+ 0, // 0: sms.v1.SmsService.Send:input_type -> sms.v1.SmsSendRequest
+ 1, // 1: sms.v1.SmsService.Send:output_type -> sms.v1.SmsSendResponse
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_sms_v1_sms_proto_init() }
+func file_sms_v1_sms_proto_init() {
+ if File_sms_v1_sms_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_sms_v1_sms_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SmsSendRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_sms_v1_sms_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SmsSendResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_sms_v1_sms_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_sms_v1_sms_proto_goTypes,
+ DependencyIndexes: file_sms_v1_sms_proto_depIdxs,
+ MessageInfos: file_sms_v1_sms_proto_msgTypes,
+ }.Build()
+ File_sms_v1_sms_proto = out.File
+ file_sms_v1_sms_proto_rawDesc = nil
+ file_sms_v1_sms_proto_goTypes = nil
+ file_sms_v1_sms_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/sms/v1/sms_grpc.pb.go b/webook/api/proto/gen/sms/v1/sms_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..41905b488f3dbc36c0599663eaa05aba02614d8f
--- /dev/null
+++ b/webook/api/proto/gen/sms/v1/sms_grpc.pb.go
@@ -0,0 +1,111 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: sms/v1/sms.proto
+
+package smsv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ SmsService_Send_FullMethodName = "/sms.v1.SmsService/Send"
+)
+
+// SmsServiceClient is the client API for SmsService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type SmsServiceClient interface {
+ // 发送消息
+ Send(ctx context.Context, in *SmsSendRequest, opts ...grpc.CallOption) (*SmsSendResponse, error)
+}
+
+type smsServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewSmsServiceClient(cc grpc.ClientConnInterface) SmsServiceClient {
+ return &smsServiceClient{cc}
+}
+
+func (c *smsServiceClient) Send(ctx context.Context, in *SmsSendRequest, opts ...grpc.CallOption) (*SmsSendResponse, error) {
+ out := new(SmsSendResponse)
+ err := c.cc.Invoke(ctx, SmsService_Send_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// SmsServiceServer is the server API for SmsService service.
+// All implementations must embed UnimplementedSmsServiceServer
+// for forward compatibility
+type SmsServiceServer interface {
+ // 发送消息
+ Send(context.Context, *SmsSendRequest) (*SmsSendResponse, error)
+ mustEmbedUnimplementedSmsServiceServer()
+}
+
+// UnimplementedSmsServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedSmsServiceServer struct {
+}
+
+func (UnimplementedSmsServiceServer) Send(context.Context, *SmsSendRequest) (*SmsSendResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
+}
+func (UnimplementedSmsServiceServer) mustEmbedUnimplementedSmsServiceServer() {}
+
+// UnsafeSmsServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to SmsServiceServer will
+// result in compilation errors.
+type UnsafeSmsServiceServer interface {
+ mustEmbedUnimplementedSmsServiceServer()
+}
+
+func RegisterSmsServiceServer(s grpc.ServiceRegistrar, srv SmsServiceServer) {
+ s.RegisterService(&SmsService_ServiceDesc, srv)
+}
+
+func _SmsService_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SmsSendRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SmsServiceServer).Send(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: SmsService_Send_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SmsServiceServer).Send(ctx, req.(*SmsSendRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// SmsService_ServiceDesc is the grpc.ServiceDesc for SmsService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var SmsService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "sms.v1.SmsService",
+ HandlerType: (*SmsServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Send",
+ Handler: _SmsService_Send_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "sms/v1/sms.proto",
+}
diff --git a/webook/api/proto/gen/tag/v1/tag.pb.go b/webook/api/proto/gen/tag/v1/tag.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..0164724b945dd3f8a0c38f290862a7f57d10d8d5
--- /dev/null
+++ b/webook/api/proto/gen/tag/v1/tag.pb.go
@@ -0,0 +1,753 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: tag/v1/tag.proto
+
+package tagv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Tag struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+ // 谁的标签,如果是全局标签(或者系统标签)
+ // 这个字段是不需要的
+ // 层级标签,你可能需要一个 oid 的东西,比如说 oid = 1 代表 IT 技术部门
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *Tag) Reset() {
+ *x = Tag{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Tag) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Tag) ProtoMessage() {}
+
+func (x *Tag) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Tag.ProtoReflect.Descriptor instead.
+func (*Tag) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Tag) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *Tag) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *Tag) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type AttachTagsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Biz string `protobuf:"bytes,1,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,2,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+ Uid int64 `protobuf:"varint,3,opt,name=uid,proto3" json:"uid,omitempty"`
+ // 因为标签本身就是跟用户有关的,你这里还要不要传一个多余的 uid??
+ Tids []int64 `protobuf:"varint,4,rep,packed,name=tids,proto3" json:"tids,omitempty"`
+}
+
+func (x *AttachTagsRequest) Reset() {
+ *x = AttachTagsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AttachTagsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttachTagsRequest) ProtoMessage() {}
+
+func (x *AttachTagsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AttachTagsRequest.ProtoReflect.Descriptor instead.
+func (*AttachTagsRequest) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AttachTagsRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *AttachTagsRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+func (x *AttachTagsRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *AttachTagsRequest) GetTids() []int64 {
+ if x != nil {
+ return x.Tids
+ }
+ return nil
+}
+
+type AttachTagsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *AttachTagsResponse) Reset() {
+ *x = AttachTagsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AttachTagsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttachTagsResponse) ProtoMessage() {}
+
+func (x *AttachTagsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AttachTagsResponse.ProtoReflect.Descriptor instead.
+func (*AttachTagsResponse) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{2}
+}
+
+type CreateTagRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *CreateTagRequest) Reset() {
+ *x = CreateTagRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateTagRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateTagRequest) ProtoMessage() {}
+
+func (x *CreateTagRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateTagRequest.ProtoReflect.Descriptor instead.
+func (*CreateTagRequest) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CreateTagRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *CreateTagRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type CreateTagResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 关键是返回一个 ID
+ // 你创建的这个标签的 ID
+ Tag *Tag `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
+}
+
+func (x *CreateTagResponse) Reset() {
+ *x = CreateTagResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateTagResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateTagResponse) ProtoMessage() {}
+
+func (x *CreateTagResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateTagResponse.ProtoReflect.Descriptor instead.
+func (*CreateTagResponse) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *CreateTagResponse) GetTag() *Tag {
+ if x != nil {
+ return x.Tag
+ }
+ return nil
+}
+
+type GetTagsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // 按照用户的 id 来查找
+ // 要不要分页?
+ // 这个地方可以不分
+ // 个人用户的标签不会很多
+ Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"`
+}
+
+func (x *GetTagsRequest) Reset() {
+ *x = GetTagsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetTagsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetTagsRequest) ProtoMessage() {}
+
+func (x *GetTagsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetTagsRequest.ProtoReflect.Descriptor instead.
+func (*GetTagsRequest) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetTagsRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+type GetTagsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Tag []*Tag `protobuf:"bytes,1,rep,name=tag,proto3" json:"tag,omitempty"`
+}
+
+func (x *GetTagsResponse) Reset() {
+ *x = GetTagsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetTagsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetTagsResponse) ProtoMessage() {}
+
+func (x *GetTagsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetTagsResponse.ProtoReflect.Descriptor instead.
+func (*GetTagsResponse) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *GetTagsResponse) GetTag() []*Tag {
+ if x != nil {
+ return x.Tag
+ }
+ return nil
+}
+
+type GetBizTagsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"`
+ Biz string `protobuf:"bytes,2,opt,name=biz,proto3" json:"biz,omitempty"`
+ BizId int64 `protobuf:"varint,3,opt,name=biz_id,json=bizId,proto3" json:"biz_id,omitempty"`
+}
+
+func (x *GetBizTagsRequest) Reset() {
+ *x = GetBizTagsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetBizTagsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBizTagsRequest) ProtoMessage() {}
+
+func (x *GetBizTagsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBizTagsRequest.ProtoReflect.Descriptor instead.
+func (*GetBizTagsRequest) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetBizTagsRequest) GetUid() int64 {
+ if x != nil {
+ return x.Uid
+ }
+ return 0
+}
+
+func (x *GetBizTagsRequest) GetBiz() string {
+ if x != nil {
+ return x.Biz
+ }
+ return ""
+}
+
+func (x *GetBizTagsRequest) GetBizId() int64 {
+ if x != nil {
+ return x.BizId
+ }
+ return 0
+}
+
+type GetBizTagsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Tags []*Tag `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty"`
+}
+
+func (x *GetBizTagsResponse) Reset() {
+ *x = GetBizTagsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_tag_v1_tag_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetBizTagsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBizTagsResponse) ProtoMessage() {}
+
+func (x *GetBizTagsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_tag_v1_tag_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBizTagsResponse.ProtoReflect.Descriptor instead.
+func (*GetBizTagsResponse) Descriptor() ([]byte, []int) {
+ return file_tag_v1_tag_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GetBizTagsResponse) GetTags() []*Tag {
+ if x != nil {
+ return x.Tags
+ }
+ return nil
+}
+
+var File_tag_v1_tag_proto protoreflect.FileDescriptor
+
+var file_tag_v1_tag_proto_rawDesc = []byte{
+ 0x0a, 0x10, 0x74, 0x61, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x67, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x12, 0x06, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x22, 0x3b, 0x0a, 0x03, 0x54, 0x61,
+ 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x62, 0x0a, 0x11, 0x41, 0x74, 0x74, 0x61, 0x63,
+ 0x68, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03,
+ 0x62, 0x69, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15,
+ 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x62, 0x69, 0x7a, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x64, 0x73, 0x18,
+ 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x64, 0x73, 0x22, 0x14, 0x0a, 0x12, 0x41,
+ 0x74, 0x74, 0x61, 0x63, 0x68, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x22, 0x38, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x11, 0x43,
+ 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x1d, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e,
+ 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22,
+ 0x22, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03,
+ 0x75, 0x69, 0x64, 0x22, 0x30, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20,
+ 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67,
+ 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x4e, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x42, 0x69, 0x7a, 0x54,
+ 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69,
+ 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03,
+ 0x62, 0x69, 0x7a, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x7a, 0x12, 0x15,
+ 0x0a, 0x06, 0x62, 0x69, 0x7a, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
+ 0x62, 0x69, 0x7a, 0x49, 0x64, 0x22, 0x35, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x69, 0x7a, 0x54,
+ 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x74,
+ 0x61, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x74, 0x61, 0x67, 0x2e,
+ 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x32, 0x94, 0x02, 0x0a,
+ 0x0a, 0x54, 0x61, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x43,
+ 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, 0x12, 0x18, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76,
+ 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61,
+ 0x74, 0x65, 0x54, 0x61, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a,
+ 0x0a, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x54, 0x61, 0x67, 0x73, 0x12, 0x19, 0x2e, 0x74, 0x61,
+ 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x54, 0x61, 0x67, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e,
+ 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x12, 0x16, 0x2e,
+ 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x47,
+ 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43,
+ 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x69, 0x7a, 0x54, 0x61, 0x67, 0x73, 0x12, 0x19, 0x2e, 0x74,
+ 0x61, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x69, 0x7a, 0x54, 0x61, 0x67, 0x73,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x74, 0x61, 0x67, 0x2e, 0x76, 0x31,
+ 0x2e, 0x47, 0x65, 0x74, 0x42, 0x69, 0x7a, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x42, 0x8e, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x61, 0x67, 0x2e,
+ 0x76, 0x31, 0x42, 0x08, 0x54, 0x61, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d,
+ 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61,
+ 0x6e, 0x67, 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f,
+ 0x6f, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
+ 0x2f, 0x74, 0x61, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x61, 0x67, 0x76, 0x31, 0xa2, 0x02, 0x03,
+ 0x54, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x54, 0x61, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x54,
+ 0x61, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, 0x54, 0x61, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47,
+ 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x54, 0x61, 0x67,
+ 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_tag_v1_tag_proto_rawDescOnce sync.Once
+ file_tag_v1_tag_proto_rawDescData = file_tag_v1_tag_proto_rawDesc
+)
+
+func file_tag_v1_tag_proto_rawDescGZIP() []byte {
+ file_tag_v1_tag_proto_rawDescOnce.Do(func() {
+ file_tag_v1_tag_proto_rawDescData = protoimpl.X.CompressGZIP(file_tag_v1_tag_proto_rawDescData)
+ })
+ return file_tag_v1_tag_proto_rawDescData
+}
+
+var file_tag_v1_tag_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_tag_v1_tag_proto_goTypes = []interface{}{
+ (*Tag)(nil), // 0: tag.v1.Tag
+ (*AttachTagsRequest)(nil), // 1: tag.v1.AttachTagsRequest
+ (*AttachTagsResponse)(nil), // 2: tag.v1.AttachTagsResponse
+ (*CreateTagRequest)(nil), // 3: tag.v1.CreateTagRequest
+ (*CreateTagResponse)(nil), // 4: tag.v1.CreateTagResponse
+ (*GetTagsRequest)(nil), // 5: tag.v1.GetTagsRequest
+ (*GetTagsResponse)(nil), // 6: tag.v1.GetTagsResponse
+ (*GetBizTagsRequest)(nil), // 7: tag.v1.GetBizTagsRequest
+ (*GetBizTagsResponse)(nil), // 8: tag.v1.GetBizTagsResponse
+}
+var file_tag_v1_tag_proto_depIdxs = []int32{
+ 0, // 0: tag.v1.CreateTagResponse.tag:type_name -> tag.v1.Tag
+ 0, // 1: tag.v1.GetTagsResponse.tag:type_name -> tag.v1.Tag
+ 0, // 2: tag.v1.GetBizTagsResponse.tags:type_name -> tag.v1.Tag
+ 3, // 3: tag.v1.TagService.CreateTag:input_type -> tag.v1.CreateTagRequest
+ 1, // 4: tag.v1.TagService.AttachTags:input_type -> tag.v1.AttachTagsRequest
+ 5, // 5: tag.v1.TagService.GetTags:input_type -> tag.v1.GetTagsRequest
+ 7, // 6: tag.v1.TagService.GetBizTags:input_type -> tag.v1.GetBizTagsRequest
+ 4, // 7: tag.v1.TagService.CreateTag:output_type -> tag.v1.CreateTagResponse
+ 2, // 8: tag.v1.TagService.AttachTags:output_type -> tag.v1.AttachTagsResponse
+ 6, // 9: tag.v1.TagService.GetTags:output_type -> tag.v1.GetTagsResponse
+ 8, // 10: tag.v1.TagService.GetBizTags:output_type -> tag.v1.GetBizTagsResponse
+ 7, // [7:11] is the sub-list for method output_type
+ 3, // [3:7] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_tag_v1_tag_proto_init() }
+func file_tag_v1_tag_proto_init() {
+ if File_tag_v1_tag_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_tag_v1_tag_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Tag); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AttachTagsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AttachTagsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateTagRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateTagResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetTagsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetTagsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetBizTagsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_tag_v1_tag_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetBizTagsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_tag_v1_tag_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 9,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_tag_v1_tag_proto_goTypes,
+ DependencyIndexes: file_tag_v1_tag_proto_depIdxs,
+ MessageInfos: file_tag_v1_tag_proto_msgTypes,
+ }.Build()
+ File_tag_v1_tag_proto = out.File
+ file_tag_v1_tag_proto_rawDesc = nil
+ file_tag_v1_tag_proto_goTypes = nil
+ file_tag_v1_tag_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/tag/v1/tag_grpc.pb.go b/webook/api/proto/gen/tag/v1/tag_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a8fc28f0a426dc71edeba2a3f6f5499c2fb220a
--- /dev/null
+++ b/webook/api/proto/gen/tag/v1/tag_grpc.pb.go
@@ -0,0 +1,228 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: tag/v1/tag.proto
+
+package tagv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ TagService_CreateTag_FullMethodName = "/tag.v1.TagService/CreateTag"
+ TagService_AttachTags_FullMethodName = "/tag.v1.TagService/AttachTags"
+ TagService_GetTags_FullMethodName = "/tag.v1.TagService/GetTags"
+ TagService_GetBizTags_FullMethodName = "/tag.v1.TagService/GetBizTags"
+)
+
+// TagServiceClient is the client API for TagService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type TagServiceClient interface {
+ CreateTag(ctx context.Context, in *CreateTagRequest, opts ...grpc.CallOption) (*CreateTagResponse, error)
+ // 覆盖式的 API
+ // 也就是直接用新的 tag 全部覆盖掉已有的 tag
+ AttachTags(ctx context.Context, in *AttachTagsRequest, opts ...grpc.CallOption) (*AttachTagsResponse, error)
+ // 我们可以预期,一个用户的标签不会有很多,所以没特别大的必要做成分页
+ GetTags(ctx context.Context, in *GetTagsRequest, opts ...grpc.CallOption) (*GetTagsResponse, error)
+ // 某个人给某个资源打了什么标签
+ GetBizTags(ctx context.Context, in *GetBizTagsRequest, opts ...grpc.CallOption) (*GetBizTagsResponse, error)
+}
+
+type tagServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewTagServiceClient(cc grpc.ClientConnInterface) TagServiceClient {
+ return &tagServiceClient{cc}
+}
+
+func (c *tagServiceClient) CreateTag(ctx context.Context, in *CreateTagRequest, opts ...grpc.CallOption) (*CreateTagResponse, error) {
+ out := new(CreateTagResponse)
+ err := c.cc.Invoke(ctx, TagService_CreateTag_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *tagServiceClient) AttachTags(ctx context.Context, in *AttachTagsRequest, opts ...grpc.CallOption) (*AttachTagsResponse, error) {
+ out := new(AttachTagsResponse)
+ err := c.cc.Invoke(ctx, TagService_AttachTags_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *tagServiceClient) GetTags(ctx context.Context, in *GetTagsRequest, opts ...grpc.CallOption) (*GetTagsResponse, error) {
+ out := new(GetTagsResponse)
+ err := c.cc.Invoke(ctx, TagService_GetTags_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *tagServiceClient) GetBizTags(ctx context.Context, in *GetBizTagsRequest, opts ...grpc.CallOption) (*GetBizTagsResponse, error) {
+ out := new(GetBizTagsResponse)
+ err := c.cc.Invoke(ctx, TagService_GetBizTags_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// TagServiceServer is the server API for TagService service.
+// All implementations must embed UnimplementedTagServiceServer
+// for forward compatibility
+type TagServiceServer interface {
+ CreateTag(context.Context, *CreateTagRequest) (*CreateTagResponse, error)
+ // 覆盖式的 API
+ // 也就是直接用新的 tag 全部覆盖掉已有的 tag
+ AttachTags(context.Context, *AttachTagsRequest) (*AttachTagsResponse, error)
+ // 我们可以预期,一个用户的标签不会有很多,所以没特别大的必要做成分页
+ GetTags(context.Context, *GetTagsRequest) (*GetTagsResponse, error)
+ // 某个人给某个资源打了什么标签
+ GetBizTags(context.Context, *GetBizTagsRequest) (*GetBizTagsResponse, error)
+ mustEmbedUnimplementedTagServiceServer()
+}
+
+// UnimplementedTagServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedTagServiceServer struct {
+}
+
+func (UnimplementedTagServiceServer) CreateTag(context.Context, *CreateTagRequest) (*CreateTagResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateTag not implemented")
+}
+func (UnimplementedTagServiceServer) AttachTags(context.Context, *AttachTagsRequest) (*AttachTagsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AttachTags not implemented")
+}
+func (UnimplementedTagServiceServer) GetTags(context.Context, *GetTagsRequest) (*GetTagsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetTags not implemented")
+}
+func (UnimplementedTagServiceServer) GetBizTags(context.Context, *GetBizTagsRequest) (*GetBizTagsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetBizTags not implemented")
+}
+func (UnimplementedTagServiceServer) mustEmbedUnimplementedTagServiceServer() {}
+
+// UnsafeTagServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to TagServiceServer will
+// result in compilation errors.
+type UnsafeTagServiceServer interface {
+ mustEmbedUnimplementedTagServiceServer()
+}
+
+func RegisterTagServiceServer(s grpc.ServiceRegistrar, srv TagServiceServer) {
+ s.RegisterService(&TagService_ServiceDesc, srv)
+}
+
+func _TagService_CreateTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateTagRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TagServiceServer).CreateTag(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TagService_CreateTag_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TagServiceServer).CreateTag(ctx, req.(*CreateTagRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TagService_AttachTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AttachTagsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TagServiceServer).AttachTags(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TagService_AttachTags_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TagServiceServer).AttachTags(ctx, req.(*AttachTagsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TagService_GetTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetTagsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TagServiceServer).GetTags(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TagService_GetTags_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TagServiceServer).GetTags(ctx, req.(*GetTagsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _TagService_GetBizTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetBizTagsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(TagServiceServer).GetBizTags(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: TagService_GetBizTags_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(TagServiceServer).GetBizTags(ctx, req.(*GetBizTagsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// TagService_ServiceDesc is the grpc.ServiceDesc for TagService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var TagService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "tag.v1.TagService",
+ HandlerType: (*TagServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "CreateTag",
+ Handler: _TagService_CreateTag_Handler,
+ },
+ {
+ MethodName: "AttachTags",
+ Handler: _TagService_AttachTags_Handler,
+ },
+ {
+ MethodName: "GetTags",
+ Handler: _TagService_GetTags_Handler,
+ },
+ {
+ MethodName: "GetBizTags",
+ Handler: _TagService_GetBizTags_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "tag/v1/tag.proto",
+}
diff --git a/webook/api/proto/gen/user/v1/user.pb.go b/webook/api/proto/gen/user/v1/user.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..e5f2bc02aefe4a4ed65f9e5dbd4acbc216161dbb
--- /dev/null
+++ b/webook/api/proto/gen/user/v1/user.pb.go
@@ -0,0 +1,1108 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc (unknown)
+// source: user/v1/user.proto
+
+package userv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type User struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
+ Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"`
+ Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
+ Phone string `protobuf:"bytes,5,opt,name=phone,proto3" json:"phone,omitempty"`
+ AboutMe string `protobuf:"bytes,6,opt,name=aboutMe,proto3" json:"aboutMe,omitempty"`
+ Ctime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=ctime,proto3" json:"ctime,omitempty"`
+ Birthday *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=birthday,proto3" json:"birthday,omitempty"`
+ WechatInfo *WechatInfo `protobuf:"bytes,9,opt,name=wechatInfo,proto3" json:"wechatInfo,omitempty"`
+}
+
+func (x *User) Reset() {
+ *x = User{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *User) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*User) ProtoMessage() {}
+
+func (x *User) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use User.ProtoReflect.Descriptor instead.
+func (*User) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *User) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *User) GetEmail() string {
+ if x != nil {
+ return x.Email
+ }
+ return ""
+}
+
+func (x *User) GetNickname() string {
+ if x != nil {
+ return x.Nickname
+ }
+ return ""
+}
+
+func (x *User) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+func (x *User) GetPhone() string {
+ if x != nil {
+ return x.Phone
+ }
+ return ""
+}
+
+func (x *User) GetAboutMe() string {
+ if x != nil {
+ return x.AboutMe
+ }
+ return ""
+}
+
+func (x *User) GetCtime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Ctime
+ }
+ return nil
+}
+
+func (x *User) GetBirthday() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Birthday
+ }
+ return nil
+}
+
+func (x *User) GetWechatInfo() *WechatInfo {
+ if x != nil {
+ return x.WechatInfo
+ }
+ return nil
+}
+
+type WechatInfo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ OpenId string `protobuf:"bytes,1,opt,name=openId,proto3" json:"openId,omitempty"`
+ UnionId string `protobuf:"bytes,2,opt,name=unionId,proto3" json:"unionId,omitempty"`
+}
+
+func (x *WechatInfo) Reset() {
+ *x = WechatInfo{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *WechatInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WechatInfo) ProtoMessage() {}
+
+func (x *WechatInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use WechatInfo.ProtoReflect.Descriptor instead.
+func (*WechatInfo) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *WechatInfo) GetOpenId() string {
+ if x != nil {
+ return x.OpenId
+ }
+ return ""
+}
+
+func (x *WechatInfo) GetUnionId() string {
+ if x != nil {
+ return x.UnionId
+ }
+ return ""
+}
+
+type SignupRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *SignupRequest) Reset() {
+ *x = SignupRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SignupRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignupRequest) ProtoMessage() {}
+
+func (x *SignupRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignupRequest.ProtoReflect.Descriptor instead.
+func (*SignupRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SignupRequest) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type SignupResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *SignupResponse) Reset() {
+ *x = SignupResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SignupResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignupResponse) ProtoMessage() {}
+
+func (x *SignupResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignupResponse.ProtoReflect.Descriptor instead.
+func (*SignupResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{3}
+}
+
+type FindOrCreateRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Phone string `protobuf:"bytes,1,opt,name=phone,proto3" json:"phone,omitempty"`
+}
+
+func (x *FindOrCreateRequest) Reset() {
+ *x = FindOrCreateRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindOrCreateRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindOrCreateRequest) ProtoMessage() {}
+
+func (x *FindOrCreateRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindOrCreateRequest.ProtoReflect.Descriptor instead.
+func (*FindOrCreateRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *FindOrCreateRequest) GetPhone() string {
+ if x != nil {
+ return x.Phone
+ }
+ return ""
+}
+
+type FindOrCreateResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *FindOrCreateResponse) Reset() {
+ *x = FindOrCreateResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindOrCreateResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindOrCreateResponse) ProtoMessage() {}
+
+func (x *FindOrCreateResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindOrCreateResponse.ProtoReflect.Descriptor instead.
+func (*FindOrCreateResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *FindOrCreateResponse) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type LoginRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
+ Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+}
+
+func (x *LoginRequest) Reset() {
+ *x = LoginRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LoginRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LoginRequest) ProtoMessage() {}
+
+func (x *LoginRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
+func (*LoginRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *LoginRequest) GetEmail() string {
+ if x != nil {
+ return x.Email
+ }
+ return ""
+}
+
+func (x *LoginRequest) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+type LoginResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *LoginResponse) Reset() {
+ *x = LoginResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LoginResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LoginResponse) ProtoMessage() {}
+
+func (x *LoginResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
+func (*LoginResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *LoginResponse) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type ProfileRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *ProfileRequest) Reset() {
+ *x = ProfileRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ProfileRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ProfileRequest) ProtoMessage() {}
+
+func (x *ProfileRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ProfileRequest.ProtoReflect.Descriptor instead.
+func (*ProfileRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *ProfileRequest) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+type ProfileResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *ProfileResponse) Reset() {
+ *x = ProfileResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ProfileResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ProfileResponse) ProtoMessage() {}
+
+func (x *ProfileResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ProfileResponse.ProtoReflect.Descriptor instead.
+func (*ProfileResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *ProfileResponse) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type UpdateNonSensitiveInfoRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *UpdateNonSensitiveInfoRequest) Reset() {
+ *x = UpdateNonSensitiveInfoRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UpdateNonSensitiveInfoRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateNonSensitiveInfoRequest) ProtoMessage() {}
+
+func (x *UpdateNonSensitiveInfoRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateNonSensitiveInfoRequest.ProtoReflect.Descriptor instead.
+func (*UpdateNonSensitiveInfoRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *UpdateNonSensitiveInfoRequest) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+type UpdateNonSensitiveInfoResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *UpdateNonSensitiveInfoResponse) Reset() {
+ *x = UpdateNonSensitiveInfoResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UpdateNonSensitiveInfoResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateNonSensitiveInfoResponse) ProtoMessage() {}
+
+func (x *UpdateNonSensitiveInfoResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[11]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateNonSensitiveInfoResponse.ProtoReflect.Descriptor instead.
+func (*UpdateNonSensitiveInfoResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{11}
+}
+
+type FindOrCreateByWechatRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Info *WechatInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"`
+}
+
+func (x *FindOrCreateByWechatRequest) Reset() {
+ *x = FindOrCreateByWechatRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindOrCreateByWechatRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindOrCreateByWechatRequest) ProtoMessage() {}
+
+func (x *FindOrCreateByWechatRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[12]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindOrCreateByWechatRequest.ProtoReflect.Descriptor instead.
+func (*FindOrCreateByWechatRequest) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *FindOrCreateByWechatRequest) GetInfo() *WechatInfo {
+ if x != nil {
+ return x.Info
+ }
+ return nil
+}
+
+type FindOrCreateByWechatResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+}
+
+func (x *FindOrCreateByWechatResponse) Reset() {
+ *x = FindOrCreateByWechatResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_user_v1_user_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindOrCreateByWechatResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindOrCreateByWechatResponse) ProtoMessage() {}
+
+func (x *FindOrCreateByWechatResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_user_v1_user_proto_msgTypes[13]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindOrCreateByWechatResponse.ProtoReflect.Descriptor instead.
+func (*FindOrCreateByWechatResponse) Descriptor() ([]byte, []int) {
+ return file_user_v1_user_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *FindOrCreateByWechatResponse) GetUser() *User {
+ if x != nil {
+ return x.User
+ }
+ return nil
+}
+
+var File_user_v1_user_proto protoreflect.FileDescriptor
+
+var file_user_v1_user_proto_rawDesc = []byte{
+ 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
+ 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
+ 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb3,
+ 0x02, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a,
+ 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73,
+ 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
+ 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x05,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61,
+ 0x62, 0x6f, 0x75, 0x74, 0x4d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x62,
+ 0x6f, 0x75, 0x74, 0x4d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x62, 0x69, 0x72, 0x74, 0x68,
+ 0x64, 0x61, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+ 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
+ 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x62, 0x69, 0x72, 0x74, 0x68, 0x64, 0x61, 0x79, 0x12,
+ 0x33, 0x0a, 0x0a, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x09, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x65,
+ 0x63, 0x68, 0x61, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74,
+ 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x3e, 0x0a, 0x0a, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x49, 0x6e,
+ 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x6e,
+ 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x6e, 0x69,
+ 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x32, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73,
+ 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x10, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e,
+ 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x13, 0x46, 0x69,
+ 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x22, 0x39, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x4f,
+ 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e,
+ 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73,
+ 0x65, 0x72, 0x22, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73,
+ 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73,
+ 0x77, 0x6f, 0x72, 0x64, 0x22, 0x32, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73,
+ 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x20, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x66,
+ 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x34, 0x0a, 0x0f, 0x50, 0x72,
+ 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a,
+ 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73,
+ 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
+ 0x22, 0x42, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x6e, 0x53, 0x65, 0x6e,
+ 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04,
+ 0x75, 0x73, 0x65, 0x72, 0x22, 0x20, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f,
+ 0x6e, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x0a, 0x1b, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72,
+ 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x79, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x65,
+ 0x63, 0x68, 0x61, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x22, 0x41,
+ 0x0a, 0x1c, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x79,
+ 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21,
+ 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75,
+ 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65,
+ 0x72, 0x32, 0xdb, 0x03, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x12, 0x16, 0x2e, 0x75, 0x73,
+ 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69,
+ 0x67, 0x6e, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c,
+ 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x75,
+ 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65,
+ 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x75, 0x73, 0x65,
+ 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74,
+ 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67,
+ 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67,
+ 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x75, 0x73, 0x65, 0x72,
+ 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x17, 0x2e, 0x75,
+ 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
+ 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x69, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x6e, 0x53, 0x65, 0x6e, 0x73,
+ 0x69, 0x74, 0x69, 0x76, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x26, 0x2e, 0x75, 0x73, 0x65, 0x72,
+ 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x6e, 0x53, 0x65, 0x6e,
+ 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x27, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61,
+ 0x74, 0x65, 0x4e, 0x6f, 0x6e, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x49, 0x6e,
+ 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x14, 0x46, 0x69,
+ 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x79, 0x57, 0x65, 0x63, 0x68,
+ 0x61, 0x74, 0x12, 0x24, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e,
+ 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x79, 0x57, 0x65, 0x63, 0x68, 0x61,
+ 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
+ 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4f, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42,
+ 0x79, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+ 0x96, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42,
+ 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f, 0x67, 0x69,
+ 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x65, 0x6b, 0x62, 0x61, 0x6e, 0x67,
+ 0x2f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x77, 0x65, 0x62, 0x6f, 0x6f, 0x6b,
+ 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x75,
+ 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03,
+ 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07,
+ 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56,
+ 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08,
+ 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_user_v1_user_proto_rawDescOnce sync.Once
+ file_user_v1_user_proto_rawDescData = file_user_v1_user_proto_rawDesc
+)
+
+func file_user_v1_user_proto_rawDescGZIP() []byte {
+ file_user_v1_user_proto_rawDescOnce.Do(func() {
+ file_user_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_user_v1_user_proto_rawDescData)
+ })
+ return file_user_v1_user_proto_rawDescData
+}
+
+var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_user_v1_user_proto_goTypes = []interface{}{
+ (*User)(nil), // 0: user.v1.User
+ (*WechatInfo)(nil), // 1: user.v1.WechatInfo
+ (*SignupRequest)(nil), // 2: user.v1.SignupRequest
+ (*SignupResponse)(nil), // 3: user.v1.SignupResponse
+ (*FindOrCreateRequest)(nil), // 4: user.v1.FindOrCreateRequest
+ (*FindOrCreateResponse)(nil), // 5: user.v1.FindOrCreateResponse
+ (*LoginRequest)(nil), // 6: user.v1.LoginRequest
+ (*LoginResponse)(nil), // 7: user.v1.LoginResponse
+ (*ProfileRequest)(nil), // 8: user.v1.ProfileRequest
+ (*ProfileResponse)(nil), // 9: user.v1.ProfileResponse
+ (*UpdateNonSensitiveInfoRequest)(nil), // 10: user.v1.UpdateNonSensitiveInfoRequest
+ (*UpdateNonSensitiveInfoResponse)(nil), // 11: user.v1.UpdateNonSensitiveInfoResponse
+ (*FindOrCreateByWechatRequest)(nil), // 12: user.v1.FindOrCreateByWechatRequest
+ (*FindOrCreateByWechatResponse)(nil), // 13: user.v1.FindOrCreateByWechatResponse
+ (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp
+}
+var file_user_v1_user_proto_depIdxs = []int32{
+ 14, // 0: user.v1.User.ctime:type_name -> google.protobuf.Timestamp
+ 14, // 1: user.v1.User.birthday:type_name -> google.protobuf.Timestamp
+ 1, // 2: user.v1.User.wechatInfo:type_name -> user.v1.WechatInfo
+ 0, // 3: user.v1.SignupRequest.user:type_name -> user.v1.User
+ 0, // 4: user.v1.FindOrCreateResponse.user:type_name -> user.v1.User
+ 0, // 5: user.v1.LoginResponse.user:type_name -> user.v1.User
+ 0, // 6: user.v1.ProfileResponse.user:type_name -> user.v1.User
+ 0, // 7: user.v1.UpdateNonSensitiveInfoRequest.user:type_name -> user.v1.User
+ 1, // 8: user.v1.FindOrCreateByWechatRequest.info:type_name -> user.v1.WechatInfo
+ 0, // 9: user.v1.FindOrCreateByWechatResponse.user:type_name -> user.v1.User
+ 2, // 10: user.v1.UserService.Signup:input_type -> user.v1.SignupRequest
+ 4, // 11: user.v1.UserService.FindOrCreate:input_type -> user.v1.FindOrCreateRequest
+ 6, // 12: user.v1.UserService.Login:input_type -> user.v1.LoginRequest
+ 8, // 13: user.v1.UserService.Profile:input_type -> user.v1.ProfileRequest
+ 10, // 14: user.v1.UserService.UpdateNonSensitiveInfo:input_type -> user.v1.UpdateNonSensitiveInfoRequest
+ 12, // 15: user.v1.UserService.FindOrCreateByWechat:input_type -> user.v1.FindOrCreateByWechatRequest
+ 3, // 16: user.v1.UserService.Signup:output_type -> user.v1.SignupResponse
+ 5, // 17: user.v1.UserService.FindOrCreate:output_type -> user.v1.FindOrCreateResponse
+ 7, // 18: user.v1.UserService.Login:output_type -> user.v1.LoginResponse
+ 9, // 19: user.v1.UserService.Profile:output_type -> user.v1.ProfileResponse
+ 11, // 20: user.v1.UserService.UpdateNonSensitiveInfo:output_type -> user.v1.UpdateNonSensitiveInfoResponse
+ 13, // 21: user.v1.UserService.FindOrCreateByWechat:output_type -> user.v1.FindOrCreateByWechatResponse
+ 16, // [16:22] is the sub-list for method output_type
+ 10, // [10:16] is the sub-list for method input_type
+ 10, // [10:10] is the sub-list for extension type_name
+ 10, // [10:10] is the sub-list for extension extendee
+ 0, // [0:10] is the sub-list for field type_name
+}
+
+func init() { file_user_v1_user_proto_init() }
+func file_user_v1_user_proto_init() {
+ if File_user_v1_user_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_user_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*User); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*WechatInfo); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SignupRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SignupResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindOrCreateRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindOrCreateResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LoginRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LoginResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ProfileRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ProfileResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UpdateNonSensitiveInfoRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UpdateNonSensitiveInfoResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindOrCreateByWechatRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_user_v1_user_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindOrCreateByWechatResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_user_v1_user_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 14,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_user_v1_user_proto_goTypes,
+ DependencyIndexes: file_user_v1_user_proto_depIdxs,
+ MessageInfos: file_user_v1_user_proto_msgTypes,
+ }.Build()
+ File_user_v1_user_proto = out.File
+ file_user_v1_user_proto_rawDesc = nil
+ file_user_v1_user_proto_goTypes = nil
+ file_user_v1_user_proto_depIdxs = nil
+}
diff --git a/webook/api/proto/gen/user/v1/user_grpc.pb.go b/webook/api/proto/gen/user/v1/user_grpc.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..057a869d977084cc8e4b90b51dfda788e0bf2866
--- /dev/null
+++ b/webook/api/proto/gen/user/v1/user_grpc.pb.go
@@ -0,0 +1,294 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc (unknown)
+// source: user/v1/user.proto
+
+package userv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ UserService_Signup_FullMethodName = "/user.v1.UserService/Signup"
+ UserService_FindOrCreate_FullMethodName = "/user.v1.UserService/FindOrCreate"
+ UserService_Login_FullMethodName = "/user.v1.UserService/Login"
+ UserService_Profile_FullMethodName = "/user.v1.UserService/Profile"
+ UserService_UpdateNonSensitiveInfo_FullMethodName = "/user.v1.UserService/UpdateNonSensitiveInfo"
+ UserService_FindOrCreateByWechat_FullMethodName = "/user.v1.UserService/FindOrCreateByWechat"
+)
+
+// UserServiceClient is the client API for UserService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type UserServiceClient interface {
+ Signup(ctx context.Context, in *SignupRequest, opts ...grpc.CallOption) (*SignupResponse, error)
+ FindOrCreate(ctx context.Context, in *FindOrCreateRequest, opts ...grpc.CallOption) (*FindOrCreateResponse, error)
+ Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
+ Profile(ctx context.Context, in *ProfileRequest, opts ...grpc.CallOption) (*ProfileResponse, error)
+ UpdateNonSensitiveInfo(ctx context.Context, in *UpdateNonSensitiveInfoRequest, opts ...grpc.CallOption) (*UpdateNonSensitiveInfoResponse, error)
+ FindOrCreateByWechat(ctx context.Context, in *FindOrCreateByWechatRequest, opts ...grpc.CallOption) (*FindOrCreateByWechatResponse, error)
+}
+
+type userServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
+ return &userServiceClient{cc}
+}
+
+func (c *userServiceClient) Signup(ctx context.Context, in *SignupRequest, opts ...grpc.CallOption) (*SignupResponse, error) {
+ out := new(SignupResponse)
+ err := c.cc.Invoke(ctx, UserService_Signup_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *userServiceClient) FindOrCreate(ctx context.Context, in *FindOrCreateRequest, opts ...grpc.CallOption) (*FindOrCreateResponse, error) {
+ out := new(FindOrCreateResponse)
+ err := c.cc.Invoke(ctx, UserService_FindOrCreate_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *userServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
+ out := new(LoginResponse)
+ err := c.cc.Invoke(ctx, UserService_Login_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *userServiceClient) Profile(ctx context.Context, in *ProfileRequest, opts ...grpc.CallOption) (*ProfileResponse, error) {
+ out := new(ProfileResponse)
+ err := c.cc.Invoke(ctx, UserService_Profile_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *userServiceClient) UpdateNonSensitiveInfo(ctx context.Context, in *UpdateNonSensitiveInfoRequest, opts ...grpc.CallOption) (*UpdateNonSensitiveInfoResponse, error) {
+ out := new(UpdateNonSensitiveInfoResponse)
+ err := c.cc.Invoke(ctx, UserService_UpdateNonSensitiveInfo_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *userServiceClient) FindOrCreateByWechat(ctx context.Context, in *FindOrCreateByWechatRequest, opts ...grpc.CallOption) (*FindOrCreateByWechatResponse, error) {
+ out := new(FindOrCreateByWechatResponse)
+ err := c.cc.Invoke(ctx, UserService_FindOrCreateByWechat_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// UserServiceServer is the server API for UserService service.
+// All implementations must embed UnimplementedUserServiceServer
+// for forward compatibility
+type UserServiceServer interface {
+ Signup(context.Context, *SignupRequest) (*SignupResponse, error)
+ FindOrCreate(context.Context, *FindOrCreateRequest) (*FindOrCreateResponse, error)
+ Login(context.Context, *LoginRequest) (*LoginResponse, error)
+ Profile(context.Context, *ProfileRequest) (*ProfileResponse, error)
+ UpdateNonSensitiveInfo(context.Context, *UpdateNonSensitiveInfoRequest) (*UpdateNonSensitiveInfoResponse, error)
+ FindOrCreateByWechat(context.Context, *FindOrCreateByWechatRequest) (*FindOrCreateByWechatResponse, error)
+ mustEmbedUnimplementedUserServiceServer()
+}
+
+// UnimplementedUserServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedUserServiceServer struct {
+}
+
+func (UnimplementedUserServiceServer) Signup(context.Context, *SignupRequest) (*SignupResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Signup not implemented")
+}
+func (UnimplementedUserServiceServer) FindOrCreate(context.Context, *FindOrCreateRequest) (*FindOrCreateResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FindOrCreate not implemented")
+}
+func (UnimplementedUserServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
+}
+func (UnimplementedUserServiceServer) Profile(context.Context, *ProfileRequest) (*ProfileResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Profile not implemented")
+}
+func (UnimplementedUserServiceServer) UpdateNonSensitiveInfo(context.Context, *UpdateNonSensitiveInfoRequest) (*UpdateNonSensitiveInfoResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method UpdateNonSensitiveInfo not implemented")
+}
+func (UnimplementedUserServiceServer) FindOrCreateByWechat(context.Context, *FindOrCreateByWechatRequest) (*FindOrCreateByWechatResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FindOrCreateByWechat not implemented")
+}
+func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
+
+// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to UserServiceServer will
+// result in compilation errors.
+type UnsafeUserServiceServer interface {
+ mustEmbedUnimplementedUserServiceServer()
+}
+
+func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
+ s.RegisterService(&UserService_ServiceDesc, srv)
+}
+
+func _UserService_Signup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SignupRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).Signup(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_Signup_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).Signup(ctx, req.(*SignupRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _UserService_FindOrCreate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FindOrCreateRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).FindOrCreate(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_FindOrCreate_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).FindOrCreate(ctx, req.(*FindOrCreateRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _UserService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(LoginRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).Login(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_Login_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).Login(ctx, req.(*LoginRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _UserService_Profile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ProfileRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).Profile(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_Profile_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).Profile(ctx, req.(*ProfileRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _UserService_UpdateNonSensitiveInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(UpdateNonSensitiveInfoRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).UpdateNonSensitiveInfo(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_UpdateNonSensitiveInfo_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).UpdateNonSensitiveInfo(ctx, req.(*UpdateNonSensitiveInfoRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _UserService_FindOrCreateByWechat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FindOrCreateByWechatRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(UserServiceServer).FindOrCreateByWechat(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: UserService_FindOrCreateByWechat_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(UserServiceServer).FindOrCreateByWechat(ctx, req.(*FindOrCreateByWechatRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var UserService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "user.v1.UserService",
+ HandlerType: (*UserServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Signup",
+ Handler: _UserService_Signup_Handler,
+ },
+ {
+ MethodName: "FindOrCreate",
+ Handler: _UserService_FindOrCreate_Handler,
+ },
+ {
+ MethodName: "Login",
+ Handler: _UserService_Login_Handler,
+ },
+ {
+ MethodName: "Profile",
+ Handler: _UserService_Profile_Handler,
+ },
+ {
+ MethodName: "UpdateNonSensitiveInfo",
+ Handler: _UserService_UpdateNonSensitiveInfo_Handler,
+ },
+ {
+ MethodName: "FindOrCreateByWechat",
+ Handler: _UserService_FindOrCreateByWechat_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "user/v1/user.proto",
+}
diff --git a/webook/api/proto/intr/v1/intr.proto b/webook/api/proto/intr/v1/intr.proto
new file mode 100644
index 0000000000000000000000000000000000000000..a41261e2df854f653ba6418b04ca9bc7b08894f3
--- /dev/null
+++ b/webook/api/proto/intr/v1/intr.proto
@@ -0,0 +1,97 @@
+syntax = "proto3";
+
+package intr.v1;
+option go_package="intr/v1;intrv1";
+
+service InteractiveService {
+ rpc IncrReadCnt(IncrReadCntRequest) returns (IncrReadCntResponse);
+ // Like 点赞
+ rpc Like(LikeRequest) returns (LikeResponse);
+ // CancelLike 取消点赞
+ rpc CancelLike(CancelLikeRequest) returns (CancelLikeResponse);
+ // Collect 收藏
+ rpc Collect(CollectRequest) returns (CollectResponse);
+ rpc Get(GetRequest) returns (GetResponse);
+ rpc GetByIds(GetByIdsRequest) returns (GetByIdsResponse);
+}
+
+message GetByIdsRequest {
+ string biz = 1;
+ repeated int64 ids = 2;
+}
+
+message GetByIdsResponse {
+ map intrs = 1;
+}
+
+message GetRequest {
+ string biz = 1;
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ int64 biz_id = 2;
+ int64 uid = 3;
+}
+
+message GetResponse {
+ Interactive intr = 1;
+}
+
+message Interactive {
+ string biz = 1;
+ int64 biz_id = 2;
+
+ int64 read_cnt = 3;
+ int64 like_cnt = 4;
+ int64 collect_cnt = 5;
+ bool liked = 6;
+ bool collected = 7;
+}
+
+message CollectRequest {
+ string biz = 1;
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ int64 biz_id = 2;
+ int64 uid = 3;
+ int64 cid = 4;
+}
+
+message CollectResponse {
+
+}
+
+message CancelLikeRequest {
+ string biz = 1;
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ int64 biz_id = 2;
+ int64 uid = 3;
+}
+
+message CancelLikeResponse {
+
+}
+
+message LikeRequest {
+ string biz = 1;
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ int64 biz_id = 2;
+ int64 uid = 3;
+}
+
+message LikeResponse {
+
+}
+
+
+message IncrReadCntRequest {
+ string biz = 1;
+ // protobuf 比较推荐下划线。你也可以用驼峰
+ int64 biz_id = 2;
+}
+
+message IncrReadCntResponse {
+ // 有些公司的规范
+ // Code
+ // Msg
+ // Data
+}
+
+
diff --git a/webook/api/proto/payment/v1/payment.proto b/webook/api/proto/payment/v1/payment.proto
new file mode 100644
index 0000000000000000000000000000000000000000..0974c54bba695f8de751ec3b0e11b381a08bcbb0
--- /dev/null
+++ b/webook/api/proto/payment/v1/payment.proto
@@ -0,0 +1,50 @@
+syntax="proto3";
+
+// buf:lint:ignore PACKAGE_DIRECTORY_MATCH
+package pmt.v1;
+option go_package="pmt/v1;pmtv1";
+
+service WechatPaymentService {
+ // buf:lint:ignore RPC_REQUEST_STANDARD_NAME
+ rpc NativePrePay(PrePayRequest) returns (NativePrePayResponse);
+ rpc GetPayment(GetPaymentRequest) returns(GetPaymentResponse);
+// rpc Prepay(PrePayRequest) returns (PrepayResponse);
+}
+
+message GetPaymentRequest {
+ string biz_trade_no = 1;
+}
+
+message GetPaymentResponse {
+// 有需要再加字段
+ PaymentStatus status = 2;
+}
+
+message PrePayRequest {
+
+ // 带一个 type,标记是扫码支付,还是 js 跳转支付,还是唤醒本地 APP
+ // type = "native"
+
+ Amount amt = 1;
+ string biz_trade_no = 2;
+ string description = 3;
+}
+
+message Amount {
+ int64 total = 1;
+ string currency = 2;
+}
+
+enum PaymentStatus {
+ PaymentStatusUnknown = 0;
+ PaymentStatusInit = 1;
+ PaymentStatusSuccess = 2;
+ PaymentStatusFailed = 3;
+ PaymentStatusRefund = 4;
+}
+
+// NativePrePayResponse 的 response 因为支付方式不同,
+// 所以响应的含义也会有不同。
+message NativePrePayResponse {
+ string code_url = 1;
+}
\ No newline at end of file
diff --git a/webook/api/proto/reward/v1/reward.proto b/webook/api/proto/reward/v1/reward.proto
new file mode 100644
index 0000000000000000000000000000000000000000..d5bcf9663aa74bc7235218b7183e60e82f565705
--- /dev/null
+++ b/webook/api/proto/reward/v1/reward.proto
@@ -0,0 +1,51 @@
+syntax = "proto3";
+
+package reward.v1;
+option go_package="reward/v1;rewardv1";
+
+service RewardService {
+ rpc PreReward(PreRewardRequest) returns (PreRewardResponse);
+ rpc GetReward(GetRewardRequest) returns (GetRewardResponse);
+}
+
+message GetRewardRequest {
+// rid 和 打赏的人
+ int64 rid = 1;
+ int64 uid = 2;
+}
+
+// 正常来说,对于外面的人来说只关心打赏成功了没
+// 不要提前定义字段,直到有需要
+message GetRewardResponse {
+ RewardStatus status =1;
+}
+
+enum RewardStatus {
+ RewardStatusUnknown = 0;
+ RewardStatusInit = 1;
+ RewardStatusPayed = 2;
+ RewardStatusFailed = 3;
+}
+
+message PreRewardRequest {
+ string biz = 1;
+ int64 biz_id = 2;
+ // 用户能够理解的,它打赏的是什么东西
+ string biz_name = 3;
+ // 被打赏的人,也就是收钱的那个
+ int64 target_uid = 4;
+ // 打赏的人,也就是付钱的那个
+ int64 uid = 5;
+ // 打赏金额
+ int64 amt = 6;
+ // 这里要不要货币?
+}
+
+message PreRewardResponse {
+// 打赏这个东西,不存在说后面换支付啥的,
+ // 或者说至少现在没有啥必要考虑
+ // 所以直接耦合了微信扫码支付的 code_url 的说法
+ string code_url = 1;
+// 打赏的 ID
+ int64 rid = 2;
+}
diff --git a/webook/api/proto/search/v1/search.proto b/webook/api/proto/search/v1/search.proto
new file mode 100644
index 0000000000000000000000000000000000000000..23daf6def7d46bf5ec8fbe6a2168c8937d73a091
--- /dev/null
+++ b/webook/api/proto/search/v1/search.proto
@@ -0,0 +1,44 @@
+
+syntax="proto3";
+
+import "search/v1/sync.proto";
+
+package search.v1;
+option go_package="search/v1;searchv1";
+
+service SearchService {
+ // 这个是最为模糊的搜索接口
+ rpc Search(SearchRequest) returns (SearchResponse);
+
+ // 你可以考虑提供业务专属接口
+ // 实践中,这部分你应该确保做到一个实习生在进来三个月之后,
+ // 就可以快速开发这种特定业务的搜索接口
+ // rpc SearchUser() returns()
+}
+
+// 业务专属接口
+service UserServiceService {
+
+}
+
+message SearchRequest {
+ string expression = 1;
+ int64 uid = 2;
+
+ // 按类目搜索
+ // repeated string categories = 3;
+}
+
+message SearchResponse {
+ // 分类展示数据
+ UserResult user = 1;
+ ArticleResult article = 2;
+}
+
+message UserResult {
+ repeated User users =1;
+}
+
+message ArticleResult {
+ repeated Article articles = 1;
+}
\ No newline at end of file
diff --git a/webook/api/proto/search/v1/sync.proto b/webook/api/proto/search/v1/sync.proto
new file mode 100644
index 0000000000000000000000000000000000000000..4e76b95ae877dfb34d12091f13ef95f44ea9bc46
--- /dev/null
+++ b/webook/api/proto/search/v1/sync.proto
@@ -0,0 +1,64 @@
+
+syntax="proto3";
+
+package search.v1;
+option go_package="search/v1;searchv1";
+
+// SyncService 在大体量的情况下,这个接口可以考虑进一步细分
+// 也就是细分为 UserSyncService 和 ArticleSyncService
+// 同步接口
+// 读写分离
+// 命令与查询分离 CQRS
+service SyncService {
+ rpc InputUser (InputUserRequest) returns (InputUserResponse);
+ rpc InputArticle (InputArticleRequest) returns (InputArticleResponse);
+ // 现在我要同步评论数据,该怎么办?
+ // rpc InputComment()
+ // 同步标签
+ // rpc InputTags()
+
+
+ // 假如说我没有这个功能
+ // 能用,但是不好用,或者说不能提供业务定制化功能
+ // 兜底
+ rpc InputAny(InputAnyRequest) returns(InputAnyResponse);
+}
+
+message InputAnyRequest {
+ string index_name = 1;
+ string doc_id = 2;
+ string data = 3;
+}
+
+message InputAnyResponse {
+
+}
+
+message InputUserRequest {
+ User user = 1;
+}
+
+message InputUserResponse {
+}
+
+message InputArticleRequest {
+ Article article = 1;
+}
+
+message InputArticleResponse {
+}
+
+message Article {
+ int64 id = 1;
+ string title = 2;
+ int32 status = 3;
+ string content = 4;
+ repeated string tags = 5;
+}
+
+message User {
+ int64 id = 1;
+ string email = 2;
+ string nickname = 3;
+ string phone = 4;
+}
\ No newline at end of file
diff --git a/webook/api/proto/tag/v1/tag.proto b/webook/api/proto/tag/v1/tag.proto
new file mode 100644
index 0000000000000000000000000000000000000000..add6e26a56664d878723423c55388debf569a944
--- /dev/null
+++ b/webook/api/proto/tag/v1/tag.proto
@@ -0,0 +1,70 @@
+syntax="proto3";
+package tag.v1;
+option go_package="tag/v1;tagv1";
+
+message Tag {
+ int64 id =1;
+ string name = 2;
+ // 谁的标签,如果是全局标签(或者系统标签)
+ // 这个字段是不需要的
+ // 层级标签,你可能需要一个 oid 的东西,比如说 oid = 1 代表 IT 技术部门
+ int64 uid = 3;
+}
+
+service TagService {
+ rpc CreateTag(CreateTagRequest) returns (CreateTagResponse);
+ // 覆盖式的 API
+ // 也就是直接用新的 tag 全部覆盖掉已有的 tag
+ rpc AttachTags(AttachTagsRequest) returns (AttachTagsResponse);
+ // 我们可以预期,一个用户的标签不会有很多,所以没特别大的必要做成分页
+ rpc GetTags(GetTagsRequest) returns (GetTagsResponse);
+ // 某个人给某个资源打了什么标签
+ rpc GetBizTags(GetBizTagsRequest) returns(GetBizTagsResponse);
+}
+
+message AttachTagsRequest {
+ string biz = 1;
+ int64 biz_id =2;
+ int64 uid = 3;
+ // 因为标签本身就是跟用户有关的,你这里还要不要传一个多余的 uid??
+ repeated int64 tids =4;
+}
+
+message AttachTagsResponse {
+
+}
+
+message CreateTagRequest {
+ int64 uid = 1;
+ string name = 2;
+}
+
+message CreateTagResponse {
+ // 关键是返回一个 ID
+ // 你创建的这个标签的 ID
+ Tag tag = 1;
+}
+
+message GetTagsRequest {
+ // 按照用户的 id 来查找
+ // 要不要分页?
+ // 这个地方可以不分
+ // 个人用户的标签不会很多
+ int64 uid = 1;
+}
+
+message GetTagsResponse {
+ repeated Tag tag = 1;
+}
+
+message GetBizTagsRequest {
+ int64 uid = 1;
+ string biz = 2;
+ int64 biz_id = 3;
+ // 要不要分页?正常用户不会给一个资源打很多标签
+ // 以防万一,你可以只找 100 个
+}
+
+message GetBizTagsResponse {
+ repeated Tag tags = 1;
+}
diff --git a/webook/app.go b/webook/app.go
new file mode 100644
index 0000000000000000000000000000000000000000..e2afc79c1d4f766277b897979f4b851fe50d6b95
--- /dev/null
+++ b/webook/app.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/events"
+ "github.com/gin-gonic/gin"
+ "github.com/robfig/cron/v3"
+)
+
+type App struct {
+ web *gin.Engine
+ consumers []events.Consumer
+ cron *cron.Cron
+}
diff --git a/webook/bff/web/reward.go b/webook/bff/web/reward.go
new file mode 100644
index 0000000000000000000000000000000000000000..c7df0dddad5772bc8d9070a028060e7b2dfb7616
--- /dev/null
+++ b/webook/bff/web/reward.go
@@ -0,0 +1,52 @@
+package web
+
+import (
+ articlev1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/article/v1"
+ rewardv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/reward/v1"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "github.com/gin-gonic/gin"
+)
+
+type RewardHandler struct {
+ client rewardv1.RewardServiceClient
+ artClient articlev1.ArticleServiceClient
+}
+
+func NewRewardHandler(client rewardv1.RewardServiceClient, artClient articlev1.ArticleServiceClient) *RewardHandler {
+ return &RewardHandler{client: client, artClient: artClient}
+}
+
+func (h *RewardHandler) RegisterRoutes(server *gin.Engine) {
+ rg := server.Group("/reward")
+ rg.POST("/detail",
+ ginx.WrapClaimsAndReq[GetRewardReq](h.GetReward))
+}
+
+type GetRewardReq struct {
+ Rid int64
+}
+
+func (h *RewardHandler) GetReward(
+ ctx *gin.Context,
+ req GetRewardReq,
+ claims ginx.UserClaims) (ginx.Result, error) {
+ resp, err := h.client.GetReward(ctx, &rewardv1.GetRewardRequest{
+ Rid: req.Rid,
+ Uid: claims.Id,
+ })
+ if err != nil {
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统错误",
+ }, err
+ }
+ return ginx.Result{
+ // 暂时也就是只需要状态
+ Data: resp.Status.String(),
+ }, nil
+}
+
+type RewardArticleReq struct {
+ Aid int64 `json:"aid"`
+ Amt int64 `json:"amt"`
+}
diff --git a/webook/comment/config/dev.yaml b/webook/comment/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fa4701e83d7d6182f2559ec23ae635dd03376bff
--- /dev/null
+++ b/webook/comment/config/dev.yaml
@@ -0,0 +1,12 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+
+etcd:
+ endpoints:
+ - "localhost:12379"
+
+grpc:
+ server:
+ port: 8098
+ etcdAddr: "localhost:12379"
+ etcdTTL: 60
\ No newline at end of file
diff --git a/webook/comment/domain/comment.go b/webook/comment/domain/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..0965ea5181afce51924223f6709c06d77fe0efe9
--- /dev/null
+++ b/webook/comment/domain/comment.go
@@ -0,0 +1,27 @@
+package domain
+
+import "time"
+
+type Comment struct {
+ Id int64 `json:"id"`
+ // 评论者
+ Commentator User `json:"user"`
+ // 评论对象
+ // 数据里面
+ Biz string `json:"biz"`
+ BizID int64 `json:"bizid"`
+ // 评论对象
+ Content string `json:"content"`
+ // 根评论
+ RootComment *Comment `json:"rootComment"`
+ // 父评论
+ ParentComment *Comment `json:"parentComment"`
+ Children []Comment `json:"children"`
+ CTime time.Time `json:"ctime"`
+ UTime time.Time `json:"utime"`
+}
+
+type User struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+}
diff --git a/webook/comment/errs/error.go b/webook/comment/errs/error.go
new file mode 100644
index 0000000000000000000000000000000000000000..c1a57ec3c08ed80fddfee6ad18f7854adfb12dbc
--- /dev/null
+++ b/webook/comment/errs/error.go
@@ -0,0 +1,9 @@
+package errs
+
+import "github.com/pkg/errors"
+
+var ParamErr = errors.New("参数错误")
+
+func NewParamErr(val string) error {
+ return errors.Wrap(ParamErr, val)
+}
diff --git a/webook/comment/grpc/comment.go b/webook/comment/grpc/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..9f32a3e6743138cf56c05a260b9c118d264f0f1c
--- /dev/null
+++ b/webook/comment/grpc/comment.go
@@ -0,0 +1,139 @@
+package grpc
+
+import (
+ "context"
+ "math"
+
+ commentv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/comment/v1"
+ "gitee.com/geekbang/basic-go/webook/comment/domain"
+ "gitee.com/geekbang/basic-go/webook/comment/service"
+
+ "google.golang.org/grpc"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type CommentServiceServer struct {
+ // 正常我都会组合这个
+ commentv1.UnimplementedCommentServiceServer
+
+ svc service.CommentService
+}
+
+func (c *CommentServiceServer) Register(server grpc.ServiceRegistrar) {
+ commentv1.RegisterCommentServiceServer(server, c)
+}
+func NewGrpcServer(svc service.CommentService) *CommentServiceServer {
+ return &CommentServiceServer{
+ svc: svc,
+ }
+}
+
+func (c *CommentServiceServer) GetMoreReplies(ctx context.Context, req *commentv1.GetMoreRepliesRequest) (*commentv1.GetMoreRepliesResponse, error) {
+ cs, err := c.svc.GetMoreReplies(ctx, req.Rid, req.MaxId, req.Limit)
+ if err != nil {
+ return nil, err
+ }
+ return &commentv1.GetMoreRepliesResponse{
+ Replies: c.toDTO(cs),
+ }, nil
+}
+
+func (c *CommentServiceServer) GetCommentList(ctx context.Context, request *commentv1.CommentListRequest) (*commentv1.CommentListResponse, error) {
+ minID := request.MinId
+ // 第一次查询,这边我们认为用户没有传
+ if minID <= 0 {
+ // 从当前最新的评论开始取
+ minID = math.MaxInt64
+ }
+ domainComments, err := c.svc.
+ GetCommentList(ctx,
+ request.GetBiz(),
+ request.GetBizid(),
+ request.GetMinId(),
+ request.GetLimit())
+ if err != nil {
+ return nil, err
+ }
+ return &commentv1.CommentListResponse{
+ Comments: c.toDTO(domainComments),
+ }, nil
+}
+
+func (c *CommentServiceServer) DeleteComment(ctx context.Context, request *commentv1.DeleteCommentRequest) (*commentv1.DeleteCommentResponse, error) {
+ err := c.svc.DeleteComment(ctx, request.Id)
+ return &commentv1.DeleteCommentResponse{}, err
+}
+
+func (c *CommentServiceServer) CreateComment(ctx context.Context, request *commentv1.CreateCommentRequest) (*commentv1.CreateCommentResponse, error) {
+ err := c.svc.CreateComment(ctx, convertToDomain(request.GetComment()))
+ return &commentv1.CreateCommentResponse{}, err
+}
+
+func (c *CommentServiceServer) toDTO(domainComments []domain.Comment) []*commentv1.Comment {
+ rpcComments := make([]*commentv1.Comment, 0, len(domainComments))
+ for _, domainComment := range domainComments {
+ rpcComment := &commentv1.Comment{
+ Id: domainComment.Id,
+ Uid: domainComment.Commentator.ID,
+ Biz: domainComment.Biz,
+ Bizid: domainComment.BizID,
+ Content: domainComment.Content,
+ Ctime: timestamppb.New(domainComment.CTime),
+ Utime: timestamppb.New(domainComment.UTime),
+ }
+ if domainComment.RootComment != nil {
+ rpcComment.RootComment = &commentv1.Comment{
+ Id: domainComment.RootComment.Id,
+ }
+ }
+ if domainComment.ParentComment != nil {
+ rpcComment.ParentComment = &commentv1.Comment{
+ Id: domainComment.ParentComment.Id,
+ }
+ }
+ rpcComments = append(rpcComments, rpcComment)
+ }
+ rpcCommentMap := make(map[int64]*commentv1.Comment, len(rpcComments))
+ for _, rpcComment := range rpcComments {
+ rpcCommentMap[rpcComment.Id] = rpcComment
+ }
+ for _, domainComment := range domainComments {
+ rpcComment := rpcCommentMap[domainComment.Id]
+ if domainComment.RootComment != nil {
+ val, ok := rpcCommentMap[domainComment.RootComment.Id]
+ if ok {
+ rpcComment.RootComment = val
+ }
+ }
+ if domainComment.ParentComment != nil {
+ val, ok := rpcCommentMap[domainComment.ParentComment.Id]
+ if ok {
+ rpcComment.ParentComment = val
+ }
+ }
+ }
+ return rpcComments
+}
+
+func convertToDomain(comment *commentv1.Comment) domain.Comment {
+ domainComment := domain.Comment{
+ Id: comment.Id,
+ Biz: comment.Biz,
+ BizID: comment.Bizid,
+ Content: comment.Content,
+ Commentator: domain.User{
+ ID: comment.Uid,
+ },
+ }
+ if comment.GetParentComment() != nil {
+ domainComment.ParentComment = &domain.Comment{
+ Id: comment.GetParentComment().GetId(),
+ }
+ }
+ if comment.GetRootComment() != nil {
+ domainComment.RootComment = &domain.Comment{
+ Id: comment.GetRootComment().GetId(),
+ }
+ }
+ return domainComment
+}
diff --git a/webook/comment/grpc/comment_grpc.go b/webook/comment/grpc/comment_grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..eaa645a4cd8a1801e69da757d457bc2befdae057
--- /dev/null
+++ b/webook/comment/grpc/comment_grpc.go
@@ -0,0 +1,25 @@
+package grpc
+
+import (
+ "context"
+ commentv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/comment/v1"
+)
+
+type CommentServiceAsyncServer struct {
+ CommentServiceServer
+}
+
+func (c *CommentServiceAsyncServer) CreateComment(ctx context.Context, request *commentv1.CreateCommentRequest) (*commentv1.CreateCommentResponse, error) {
+ if ctx.Value("limited") == "true" || ctx.Value("downgrad") == "true" {
+ // 在这里发送到 Kafka 里面
+ return &commentv1.CreateCommentResponse{}, nil
+ } else {
+ err := c.svc.CreateComment(ctx, convertToDomain(request.GetComment()))
+ return &commentv1.CreateCommentResponse{}, err
+ }
+}
+
+func (c *CommentServiceAsyncServer) CreateCommentV1(ctx context.Context, request *commentv1.CreateCommentRequest) (*commentv1.CreateCommentResponse, error) {
+ // 在这里发送到 Kafka 里面
+ return &commentv1.CreateCommentResponse{}, nil
+}
diff --git a/webook/comment/grpc/comment_limit.go b/webook/comment/grpc/comment_limit.go
new file mode 100644
index 0000000000000000000000000000000000000000..1d6a34ff713416ccabb716beb323500bb21da067
--- /dev/null
+++ b/webook/comment/grpc/comment_limit.go
@@ -0,0 +1,31 @@
+package grpc
+
+import (
+ "context"
+ "errors"
+ commentv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/comment/v1"
+)
+
+type RateLimitCommentService struct {
+ CommentServiceServer
+}
+
+func (c *RateLimitCommentService) GetCommentList(ctx context.Context, request *commentv1.CommentListRequest) (*commentv1.CommentListResponse, error) {
+ if ctx.Value("downgrade") == "true" && !c.hotBiz(request.Biz, request.GetBizid()) {
+ return nil, errors.New("触发了降级,非热门资源")
+ }
+ return c.CommentServiceServer.GetCommentList(ctx, request)
+}
+
+func (c *RateLimitCommentService) GetMoreReplies(ctx context.Context, req *commentv1.GetMoreRepliesRequest) (*commentv1.GetMoreRepliesResponse, error) {
+ if ctx.Value("downgrade") == "true" {
+ return nil, errors.New("触发限流")
+ }
+ return c.CommentServiceServer.GetMoreReplies(ctx, req)
+}
+
+func (c *RateLimitCommentService) hotBiz(biz string, bizId int64) bool {
+ // 这个热门资源怎么判定
+ // 一般是借助周期性的任务来计算一个白名单,放进去 redis 里面。
+ return true
+}
diff --git a/webook/comment/integration/startup/db.go b/webook/comment/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..53d6e3452da6e61c2cc3b7f0060ba02db1cc155a
--- /dev/null
+++ b/webook/comment/integration/startup/db.go
@@ -0,0 +1,44 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook_comment"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ db = db.Debug()
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/comment/integration/startup/wire.go b/webook/comment/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a4879b1413ad3920859fa1b17cbd07b1ed9262b
--- /dev/null
+++ b/webook/comment/integration/startup/wire.go
@@ -0,0 +1,29 @@
+//go:build wireinject
+
+package startup
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/comment/grpc"
+ "gitee.com/geekbang/basic-go/webook/comment/repository"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/comment/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewCommentDAO,
+ repository.NewCommentRepo,
+ service.NewCommentSvc,
+ grpc2.NewGrpcServer,
+)
+
+var thirdProvider = wire.NewSet(
+ logger.NewNoOpLogger,
+ InitTestDB,
+)
+
+func InitGRPCServer() *grpc2.CommentServiceServer {
+ wire.Build(thirdProvider, serviceProviderSet)
+ return new(grpc2.CommentServiceServer)
+}
diff --git a/webook/comment/integration/startup/wire_gen.go b/webook/comment/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..70525d2e852852ebc6386bcdc02e85886e4b402e
--- /dev/null
+++ b/webook/comment/integration/startup/wire_gen.go
@@ -0,0 +1,34 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/comment/grpc"
+ "gitee.com/geekbang/basic-go/webook/comment/repository"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/comment/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitGRPCServer() *grpc.CommentServiceServer {
+ gormDB := InitTestDB()
+ commentDAO := dao.NewCommentDAO(gormDB)
+ loggerV1 := logger.NewNoOpLogger()
+ commentRepository := repository.NewCommentRepo(commentDAO, loggerV1)
+ commentService := service.NewCommentSvc(commentRepository)
+ commentServiceServer := grpc.NewGrpcServer(commentService)
+ return commentServiceServer
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewCommentDAO, repository.NewCommentRepo, service.NewCommentSvc, grpc.NewGrpcServer)
+
+var thirdProvider = wire.NewSet(logger.NewNoOpLogger, InitTestDB)
diff --git a/webook/comment/ioc/db.go b/webook/comment/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..0df9913b0699b27c3cf3c9ffa19f6be151d678c4
--- /dev/null
+++ b/webook/comment/ioc/db.go
@@ -0,0 +1,76 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ glogger "gorm.io/gorm/logger"
+ "gorm.io/plugin/opentelemetry/tracing"
+ "gorm.io/plugin/prometheus"
+)
+
+func InitDB(l logger.LoggerV1) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{
+ //使用 DEBUG 来打印
+ Logger: glogger.Default.LogMode(glogger.Info),
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // 接入 prometheus
+ err = db.Use(prometheus.New(prometheus.Config{
+ DBName: "webook",
+ // 每 15 秒采集一些数据
+ RefreshInterval: 15,
+ MetricsCollector: []prometheus.MetricsCollector{
+ &prometheus.MySQL{
+ VariableNames: []string{"Threads_running"},
+ },
+ }, // user defined metrics
+ }))
+ if err != nil {
+ panic(err)
+ }
+ err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics()))
+ if err != nil {
+ panic(err)
+ }
+
+ //prom := prometheus2.Callbacks{
+ // Namespace: "geekbang_daming",
+ // Subsystem: "webook",
+ // Name: "gorm",
+ // InstanceID: "my-instance-1",
+ // Help: "gorm DB 查询",
+ //}
+ //err = prom.Register(db)
+ //if err != nil {
+ // panic(err)
+ //}
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
diff --git a/webook/comment/ioc/etcd.go b/webook/comment/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/comment/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/comment/ioc/grpc.go b/webook/comment/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..238b64f6703e4f4031962df1fac3a5975606a1f2
--- /dev/null
+++ b/webook/comment/ioc/grpc.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/comment/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(comment *grpc2.CommentServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ comment.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "user",
+ L: l,
+ EtcdTTL: cfg.EtcdTTL,
+ EtcdClient: ecli,
+ }
+}
diff --git a/webook/comment/ioc/log.go b/webook/comment/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..cdcdb8e1de46c46662d262629031bc5af0f22727
--- /dev/null
+++ b/webook/comment/ioc/log.go
@@ -0,0 +1,39 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "gopkg.in/natefinch/lumberjack.v2"
+)
+
+func InitLogger() logger.LoggerV1 {
+
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ //cfg := zap.NewDevelopmentConfig()
+
+ //cfg.OutputPaths = []string{"./logs/app.log"}
+ //err := viper.UnmarshalKey("log", &cfg)
+ //if err != nil {
+ // panic(err)
+ //}
+
+ lumberLogger := &lumberjack.Logger{
+ // 要注意,得有权限
+ Filename: "/var/log/comment.log",
+ MaxSize: 50,
+ MaxBackups: 3,
+ MaxAge: 7,
+ }
+
+ core := zapcore.NewCore(
+ zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
+ zapcore.AddSync(lumberLogger),
+ zapcore.DebugLevel,
+ )
+
+ l := zap.New(core)
+
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/comment/main.go b/webook/comment/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..24c15957b528f086d0227b679eed4c136bc271c0
--- /dev/null
+++ b/webook/comment/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ err := app.server.Serve()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/config.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+type App struct {
+ server *grpcx.Server
+}
diff --git a/webook/comment/repository/comment.go b/webook/comment/repository/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..10f6c5da5ae6c44a11031c21ea0af75e816b6f01
--- /dev/null
+++ b/webook/comment/repository/comment.go
@@ -0,0 +1,165 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/comment/domain"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "golang.org/x/sync/errgroup"
+ "time"
+)
+
+type CommentRepository interface {
+ // FindByBiz 根据 ID 倒序查找
+ // 并且会返回每个评论的三条直接回复
+ FindByBiz(ctx context.Context, biz string,
+ bizId, minID, limit int64) ([]domain.Comment, error)
+ // DeleteComment 删除评论,删除本评论何其子评论
+ DeleteComment(ctx context.Context, comment domain.Comment) error
+ // CreateComment 创建评论
+ CreateComment(ctx context.Context, comment domain.Comment) error
+ // GetCommentByIds 获取单条评论 支持批量获取
+ GetCommentByIds(ctx context.Context, id []int64) ([]domain.Comment, error)
+ GetMoreReplies(ctx context.Context, rid int64, maxID int64, limit int64) ([]domain.Comment, error)
+}
+
+type CachedCommentRepo struct {
+ dao dao.CommentDAO
+ l logger.LoggerV1
+}
+
+func (c *CachedCommentRepo) GetMoreReplies(ctx context.Context, rid int64, maxID int64, limit int64) ([]domain.Comment, error) {
+ cs, err := c.dao.FindRepliesByRID(ctx, rid, maxID, limit)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]domain.Comment, 0, len(cs))
+ for _, cm := range cs {
+ res = append(res, c.toDomain(cm))
+ }
+ return res, nil
+}
+
+func (c *CachedCommentRepo) FindByBiz(ctx context.Context, biz string,
+ bizId, minID, limit int64) ([]domain.Comment, error) {
+ // 事实上,最新评论它的缓存效果不是很好
+ // 在这里缓存第一页,缓存咩有,就去找数据库
+ // 也可以考虑定时刷新缓存
+ // 拿到的就是顶级评论
+ daoComments, err := c.dao.FindByBiz(ctx, biz, bizId, minID, limit)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]domain.Comment, 0, len(daoComments))
+ // 拿到前三条子评论
+ // 按照 pid 来分组,取组内三条(这三条是按照 ID 降序排序)
+ // SELECT * FROM `comments` WHERE pid IN $ids GROUP BY pid ORDER BY id DESC LIMIT 3;
+ var eg errgroup.Group
+ for _, dc := range daoComments {
+ dc := dc
+ current := c.toDomain(dc)
+ res = append(res, current)
+ // 降级不需要去查询子评论
+ if ctx.Value("downgrade") == "true" {
+ // 尤其要关注数据库的读压力
+ continue
+ }
+ eg.Go(func() error {
+ // 去数据库查询
+ // 取三条回复
+ subCs, err := c.dao.FindRepliesByPID(ctx, dc.ID, 0, 3)
+ if err != nil {
+ return err
+ }
+ current.Children = make([]domain.Comment, 0, len(subCs))
+ // 不然呢?
+ for _, sc := range subCs {
+ // 构建子评论
+ current.Children = append(current.Children, c.toDomain(sc))
+ }
+ return nil
+ })
+ }
+ return res, eg.Wait()
+}
+
+func (c *CachedCommentRepo) DeleteComment(ctx context.Context, comment domain.Comment) error {
+ return c.dao.Delete(ctx, dao.Comment{
+ ID: comment.Id,
+ })
+}
+
+func (c *CachedCommentRepo) CreateComment(ctx context.Context, comment domain.Comment) error {
+ return c.dao.Insert(ctx, c.toEntity(comment))
+}
+
+func (c *CachedCommentRepo) GetCommentByIds(ctx context.Context, ids []int64) ([]domain.Comment, error) {
+ vals, err := c.dao.FindOneByIDs(ctx, ids)
+ if err != nil {
+ return nil, err
+ }
+ comments := make([]domain.Comment, 0, len(vals))
+ for _, v := range vals {
+ comment := c.toDomain(v)
+ comments = append(comments, comment)
+ }
+ return comments, nil
+}
+
+func (c *CachedCommentRepo) toDomain(daoComment dao.Comment) domain.Comment {
+ val := domain.Comment{
+ Id: daoComment.ID,
+ Commentator: domain.User{
+ ID: daoComment.Uid,
+ },
+ Biz: daoComment.Biz,
+ BizID: daoComment.BizID,
+ Content: daoComment.Content,
+ CTime: time.UnixMilli(daoComment.Ctime),
+ UTime: time.UnixMilli(daoComment.Utime),
+ }
+ if daoComment.PID.Valid {
+ val.ParentComment = &domain.Comment{
+ Id: daoComment.PID.Int64,
+ }
+ }
+ if daoComment.RootID.Valid {
+ val.RootComment = &domain.Comment{
+ Id: daoComment.RootID.Int64,
+ }
+ }
+ return val
+}
+
+func (c *CachedCommentRepo) toEntity(domainComment domain.Comment) dao.Comment {
+ daoComment := dao.Comment{
+ ID: domainComment.Id,
+ Uid: domainComment.Commentator.ID,
+ Biz: domainComment.Biz,
+ BizID: domainComment.BizID,
+ Content: domainComment.Content,
+ }
+ if domainComment.RootComment != nil {
+ daoComment.RootID = sql.NullInt64{
+ Valid: true,
+ Int64: domainComment.RootComment.Id,
+ }
+ }
+ if domainComment.ParentComment != nil {
+ daoComment.PID = sql.NullInt64{
+ Valid: true,
+ Int64: domainComment.ParentComment.Id,
+ }
+ }
+ daoComment.Ctime = time.Now().UnixMilli()
+ daoComment.Utime = time.Now().UnixMilli()
+ return daoComment
+}
+
+func NewCommentRepo(commentDAO dao.CommentDAO, l logger.LoggerV1) CommentRepository {
+ return &CachedCommentRepo{
+ dao: commentDAO,
+ l: l,
+ }
+}
diff --git a/webook/comment/repository/dao/comment.go b/webook/comment/repository/dao/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..4566b0befce9eb646786f8644e9233791f84be6e
--- /dev/null
+++ b/webook/comment/repository/dao/comment.go
@@ -0,0 +1,185 @@
+package dao
+
+import (
+ "context"
+ "database/sql"
+ "gorm.io/gorm"
+)
+
+// ErrDataNotFound 通用的数据没找到
+var ErrDataNotFound = gorm.ErrRecordNotFound
+
+//go:generate mockgen -source=./comment.go -package=daomocks -destination=mocks/comment.mock.go CommentDAO
+type CommentDAO interface {
+ Insert(ctx context.Context, u Comment) error
+ // FindByBiz 只查找一级评论
+ FindByBiz(ctx context.Context, biz string,
+ bizID, minID, limit int64) ([]Comment, error)
+ // FindCommentList Comment的ID为0 获取一级评论,如果不为0获取对应的评论,和其评论的所有回复
+ FindCommentList(ctx context.Context, u Comment) ([]Comment, error)
+ FindRepliesByPID(ctx context.Context, pID int64, offset, limit int) ([]Comment, error)
+ // Delete 删除本节点和其对应的子节点
+ Delete(ctx context.Context, u Comment) error
+ FindOneByIDs(ctx context.Context, ID []int64) ([]Comment, error)
+ FindRepliesByRID(ctx context.Context, rID int64, ID int64, limit int64) ([]Comment, error)
+}
+
+type GORMCommentDAO struct {
+ db *gorm.DB
+}
+
+func (c *GORMCommentDAO) FindRepliesByRID(ctx context.Context,
+ rID int64, maxId int64, limit int64) ([]Comment, error) {
+ var res []Comment
+ err := c.db.WithContext(ctx).
+ Where("root_ID = ? AND ID > ?", rID, maxId).
+ Order("ID ASC").
+ Limit(int(limit)).Find(&res).Error
+ return res, err
+}
+
+func NewCommentDAO(db *gorm.DB) CommentDAO {
+ return &GORMCommentDAO{
+ db: db,
+ }
+}
+
+func (c *GORMCommentDAO) FindOneByIDs(ctx context.Context, IDs []int64) ([]Comment, error) {
+ var res []Comment
+ err := c.db.WithContext(ctx).
+ Where("ID in ?", IDs).
+ First(&res).
+ Error
+ return res, err
+}
+
+func (c *GORMCommentDAO) FindByBiz(ctx context.Context, biz string,
+ bizID, minID, limit int64) ([]Comment, error) {
+ var res []Comment
+ err := c.db.WithContext(ctx).
+ // 我只要顶级评论
+ Where("biz = ? AND biz_ID = ? AND id < ? AND pid IS NULL", biz, bizID, minID).
+ Limit(int(limit)).
+ Find(&res).Error
+ return res, err
+}
+
+// FindRepliesByPID 查找评论的直接评论
+func (c *GORMCommentDAO) FindRepliesByPID(ctx context.Context,
+ pid int64,
+ offset,
+ limit int) ([]Comment, error) {
+ var res []Comment
+ err := c.db.WithContext(ctx).Where("pid = ?", pid).
+ Order("ID DESC").
+ Offset(offset).Limit(limit).Find(&res).Error
+ return res, err
+}
+
+func (c *GORMCommentDAO) Insert(ctx context.Context, u Comment) error {
+ return c.db.
+ WithContext(ctx).
+ Create(&u).
+ Error
+}
+
+func (c *GORMCommentDAO) FindCommentList(ctx context.Context, u Comment) ([]Comment, error) {
+ var res []Comment
+ builder := c.db.WithContext(ctx)
+ if u.ID == 0 {
+ builder = builder.
+ Where("biz=?", u.Biz).
+ Where("biz_ID=?", u.BizID).
+ Where("root_ID is null")
+ } else {
+ builder = builder.Where("root_ID=? or id =?", u.ID, u.ID)
+ }
+ err := builder.Find(&res).Error
+ return res, err
+
+}
+
+func (c *GORMCommentDAO) Delete(ctx context.Context, u Comment) error {
+ // 数据库帮你级联删除了,不需要担忧并发问题
+ // 假如 4 已经删了,按照外键的约束,如果你插入一个 pid=4 的行,你是插不进去的
+ return c.db.WithContext(ctx).Delete(&Comment{
+ ID: u.ID,
+ }).Error
+}
+
+// Comment 总结:所有的索引设计,都是针对 WHERE,ORDER BY,SELECT xxx 来进行的
+// 如果有 JOIN,那么还要考虑 ON
+// 永远考虑最频繁的查询
+// 在没有遇到更新、查询性能瓶颈之前,不需要太过于担忧维护索引的开销
+// 有一些时候,随着业务发展,有一些索引用不上了,要及时删除
+type Comment struct {
+ // 代表你评论本体
+ ID int64
+ // 发表评论的人
+ // 要不要在这个列创建索引?
+ // 取决于有没有 WHERE uID = ? 的查询
+ Uid int64
+ // 这个代表的是你评论的对象是什么?
+ // 比如说代表某个帖子,代表某个视频,代表某个图片
+ Biz string `gorm:"index:biz_type_id"`
+ BizID int64 `gorm:"index:biz_type_if"`
+
+ // 用 NULL 来表达没有父亲
+ // 你可以考虑用 -1 来代表没有父亲
+ // 索引是如何处理 NULL 的???
+ // NULL 的取值非常多
+
+ PID sql.NullInt64 `gorm:"index"`
+ // 外键指向的也是同一张表
+ ParentComment *Comment `gorm:"ForeignKey:PID;AssociationForeignKey:ID;constraint:OnDelete:CASCADE"`
+
+ // 引入 RootID 这个设计
+ // 顶级评论的 ID
+ // 主要是为了加载整棵评论的回复组成树
+ RootID sql.NullInt64 `gorm:"index:root_ID_ctime"`
+ Ctime int64 `gorm:"index:root_ID_ctime"`
+
+ // 评论的内容
+ Content string
+
+ Utime int64
+}
+
+//type TreeNode struct {
+// PID int64
+// RootID int64
+//}
+//
+//type Organization struct {
+// // 这边就具备了构建树形结构的必要的字段
+// TreeNode
+//}
+
+//func ToTree(data []Organization) *TreeNode {
+//
+//}
+
+func (*Comment) TableName() string {
+ return "comments"
+}
+
+//type UserDAO interface {
+// // @sql SELECT * FROM `users` WHERE `id`=$id
+// GetByID(ctx context.Context, id int64) (*User, error)
+// // @sql SELETCT * FROM `users` WHERE region = $region OFFSET $offset LIMIT $limit
+// ListByRegion(ctx context.Context, region string, offset, limit int64) ([]*User, error)
+//}
+
+// 用 AST 解析 UserDAO 的定义
+
+// 结合代码生成技术(GO 模板编程)
+//type UserDAOImple struct {
+// db *sql.DB
+//}
+
+// EXPLAIN SELECT * xxxx...
+// 返回一个预估的行数
+
+//type UserDAO struct {
+// GetByID func(ctx context.Context, id int64) (*User, error) `sql:"SELECT xx WHERE id = ? "`
+//}
diff --git a/webook/comment/repository/dao/init_tables.go b/webook/comment/repository/dao/init_tables.go
new file mode 100644
index 0000000000000000000000000000000000000000..8a6c74084f77a84a44e29f334ae0ec5873367469
--- /dev/null
+++ b/webook/comment/repository/dao/init_tables.go
@@ -0,0 +1,7 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(&Comment{})
+}
diff --git a/webook/comment/service/comment.go b/webook/comment/service/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd9683b9ae56aac452b1770264ff223e03c9ed8d
--- /dev/null
+++ b/webook/comment/service/comment.go
@@ -0,0 +1,50 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/comment/domain"
+ "gitee.com/geekbang/basic-go/webook/comment/repository"
+)
+
+type CommentService interface {
+ // GetCommentList Comment的id为0 获取一级评论
+ // 按照 ID 倒序排序
+ GetCommentList(ctx context.Context, biz string, bizId, minID, limit int64) ([]domain.Comment, error)
+ // DeleteComment 删除评论,删除本评论何其子评论
+ DeleteComment(ctx context.Context, id int64) error
+ // CreateComment 创建评论
+ CreateComment(ctx context.Context, comment domain.Comment) error
+ GetMoreReplies(ctx context.Context, rid int64, minID int64, limit int64) ([]domain.Comment, error)
+}
+
+type commentService struct {
+ repo repository.CommentRepository
+}
+
+func (c *commentService) GetMoreReplies(ctx context.Context,
+ rid int64,
+ maxID int64, limit int64) ([]domain.Comment, error) {
+ return c.repo.GetMoreReplies(ctx, rid, maxID, limit)
+}
+
+func NewCommentSvc(repo repository.CommentRepository) CommentService {
+ return &commentService{
+ repo: repo,
+ }
+}
+
+func (c *commentService) GetCommentList(ctx context.Context, biz string,
+ bizId, minID, limit int64) ([]domain.Comment, error) {
+ list, err := c.repo.FindByBiz(ctx, biz, bizId, minID, limit)
+ return list, err
+}
+
+func (c *commentService) DeleteComment(ctx context.Context, id int64) error {
+ return c.repo.DeleteComment(ctx, domain.Comment{
+ Id: id,
+ })
+}
+
+func (c *commentService) CreateComment(ctx context.Context, comment domain.Comment) error {
+ return c.repo.CreateComment(ctx, comment)
+}
diff --git a/webook/comment/wire.go b/webook/comment/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..ae34f583477945bd2c8e0a5277ee28142b1b4c79
--- /dev/null
+++ b/webook/comment/wire.go
@@ -0,0 +1,35 @@
+//go:build wireinject
+
+package main
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/comment/grpc"
+ "gitee.com/geekbang/basic-go/webook/comment/ioc"
+ "gitee.com/geekbang/basic-go/webook/comment/repository"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/comment/service"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewCommentDAO,
+ repository.NewCommentRepo,
+ service.NewCommentSvc,
+ grpc2.NewGrpcServer,
+)
+
+var thirdProvider = wire.NewSet(
+ ioc.InitLogger,
+ ioc.InitDB,
+ ioc.InitEtcdClient,
+)
+
+func Init() *App {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ ioc.InitGRPCxServer,
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/comment/wire_gen.go b/webook/comment/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..56ffd62bc18f63ea42174953568b6ce8317b7640
--- /dev/null
+++ b/webook/comment/wire_gen.go
@@ -0,0 +1,39 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/comment/grpc"
+ "gitee.com/geekbang/basic-go/webook/comment/ioc"
+ "gitee.com/geekbang/basic-go/webook/comment/repository"
+ "gitee.com/geekbang/basic-go/webook/comment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/comment/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func Init() *App {
+ loggerV1 := ioc.InitLogger()
+ db := ioc.InitDB(loggerV1)
+ commentDAO := dao.NewCommentDAO(db)
+ commentRepository := repository.NewCommentRepo(commentDAO, loggerV1)
+ commentService := service.NewCommentSvc(commentRepository)
+ commentServiceServer := grpc.NewGrpcServer(commentService)
+ client := ioc.InitEtcdClient()
+ server := ioc.InitGRPCxServer(commentServiceServer, client, loggerV1)
+ app := &App{
+ server: server,
+ }
+ return app
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewCommentDAO, repository.NewCommentRepo, service.NewCommentSvc, grpc.NewGrpcServer)
+
+var thirdProvider = wire.NewSet(ioc.InitLogger, ioc.InitDB, ioc.InitEtcdClient)
diff --git a/webook/config/dev.go b/webook/config/dev.go
new file mode 100644
index 0000000000000000000000000000000000000000..4823122c277cd5fc52a7c4322387c96a3b869fb4
--- /dev/null
+++ b/webook/config/dev.go
@@ -0,0 +1,18 @@
+//go:build !k8s
+
+// asdsf go:build dev
+// sdd go:build test
+// dsf 34
+
+// 没有k8s 这个编译标签
+package config
+
+var Config = config{
+ DB: DBConfig{
+ // 本地连接
+ DSN: "root:root@tcp(localhost:13316)/webook",
+ },
+ Redis: RedisConfig{
+ Addr: "localhost:6379",
+ },
+}
diff --git a/webook/config/dev.yaml b/webook/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7a9d2496aa3d7c167c1e9c4b447a20b2b5ded5c3
--- /dev/null
+++ b/webook/config/dev.yaml
@@ -0,0 +1,30 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+
+redis:
+ addr: "localhost:6379"
+
+abc: "helloabc" # v1
+# abc: "helloabcdef" # v2
+
+kafka:
+ addrs:
+ - "localhost:9094"
+
+etcd:
+ endpoints:
+ - "localhost:12379"
+
+grpc:
+ client:
+ intr:
+ name: "interactive"
+ secure: false
+
+# 这个是流量控制的 client 的配置
+#grpc:
+# client:
+# intr:
+# addr: "localhost:8090"
+# secure: false
+# threshold: 100
\ No newline at end of file
diff --git a/webook/config/filebeat.yml b/webook/config/filebeat.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9348b05eb1ad1fe775f49a82c52a25856aac7f64
--- /dev/null
+++ b/webook/config/filebeat.yml
@@ -0,0 +1,280 @@
+###################### Filebeat Configuration Example #########################
+
+# This file is an example configuration file highlighting only the most common
+# options. The filebeat.reference.yml file from the same directory contains all the
+# supported options with more comments. You can use it as a reference.
+#
+# You can find the full configuration reference here:
+# https://www.elastic.co/guide/en/beats/filebeat/index.html
+
+# For more available modules and options, please see the filebeat.reference.yml sample
+# configuration file.
+
+# ============================== Filebeat inputs ===============================
+
+filebeat.inputs:
+
+# Each - is an input. Most options can be set at the input level, so
+# you can use different inputs for various configurations.
+# Below are the input specific configurations.
+
+- type: log
+
+ # Change to true to enable this input configuration.
+ enabled: true
+
+ # Paths that should be crawled and fetched. Glob based paths.
+ paths:
+ # filebeat 要对这个目录有读权限
+ - /var/log/*.log
+ # - 改成你自己的目录
+ #- c:\programdata\elasticsearch\logs\*
+
+ # Exclude lines. A list of regular expressions to match. It drops the lines that are
+ # matching any regular expression from the list.
+ #exclude_lines: ['^DBG']
+
+ # Include lines. A list of regular expressions to match. It exports the lines that are
+ # matching any regular expression from the list.
+ #include_lines: ['^ERR', '^WARN']
+
+ # Exclude files. A list of regular expressions to match. Filebeat drops the files that
+ # are matching any regular expression from the list. By default, no files are dropped.
+ #exclude_files: ['.gz$']
+
+ # Optional additional fields. These fields can be freely picked
+ # to add additional information to the crawled log files for filtering
+ #fields:
+ # level: debug
+ # review: 1
+
+ ### Multiline options
+
+ # Multiline can be used for log messages spanning multiple lines. This is common
+ # for Java Stack Traces or C-Line Continuation
+
+ # The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
+ #multiline.pattern: ^\[
+
+ # Defines if the pattern set under pattern should be negated or not. Default is false.
+ #multiline.negate: false
+
+ # Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
+ # that was (not) matched before or after or as long as a pattern is not matched based on negate.
+ # Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
+ #multiline.match: after
+
+# filestream is an experimental input. It is going to replace log input in the future.
+- type: filestream
+
+ # Change to true to enable this input configuration.
+ enabled: false
+
+ # Paths that should be crawled and fetched. Glob based paths.
+ paths:
+ - /var/log/*.log
+ #- c:\programdata\elasticsearch\logs\*
+
+ # Exclude lines. A list of regular expressions to match. It drops the lines that are
+ # matching any regular expression from the list.
+ #exclude_lines: ['^DBG']
+
+ # Include lines. A list of regular expressions to match. It exports the lines that are
+ # matching any regular expression from the list.
+ #include_lines: ['^ERR', '^WARN']
+
+ # Exclude files. A list of regular expressions to match. Filebeat drops the files that
+ # are matching any regular expression from the list. By default, no files are dropped.
+ #prospector.scanner.exclude_files: ['.gz$']
+
+ # Optional additional fields. These fields can be freely picked
+ # to add additional information to the crawled log files for filtering
+ #fields:
+ # level: debug
+ # review: 1
+
+# ============================== Filebeat modules ==============================
+
+filebeat.config.modules:
+ # Glob pattern for configuration loading
+ path: ${path.config}/modules.d/*.yml
+
+ # Set to true to enable config reloading
+ reload.enabled: false
+
+ # Period on which files under path should be checked for changes
+ #reload.period: 10s
+
+# ======================= Elasticsearch template setting =======================
+
+# setup.template.settings:
+ # index.number_of_shards: 1
+ #index.codec: best_compression
+ #_source.enabled: false
+
+
+# ================================== General ===================================
+
+# The name of the shipper that publishes the network data. It can be used to group
+# all the transactions sent by a single shipper in the web interface.
+#name:
+
+# The tags of the shipper are included in their own field with each
+# transaction published.
+#tags: ["service-X", "web-tier"]
+
+# Optional fields that you can specify to add additional information to the
+# output.
+#fields:
+# env: staging
+
+# ================================= Dashboards =================================
+# These settings control loading the sample dashboards to the Kibana index. Loading
+# the dashboards is disabled by default and can be enabled either by setting the
+# options here or by using the `setup` command.
+#setup.dashboards.enabled: false
+
+# The URL from where to download the dashboards archive. By default this URL
+# has a value which is computed based on the Beat name and version. For released
+# versions, this URL points to the dashboard archive on the artifacts.elastic.co
+# website.
+#setup.dashboards.url:
+
+# =================================== Kibana ===================================
+
+# Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API.
+# This requires a Kibana endpoint configuration.
+setup.kibana:
+# filebeat 没有运行在 docker
+# kibana:5601
+ host: "localhost:5601"
+
+ # Kibana Host
+ # Scheme and port can be left out and will be set to the default (http and 5601)
+ # In case you specify and additional path, the scheme is required: http://localhost:5601/path
+ # IPv6 addresses should always be defined as: https://[2001:db8::1]:5601
+ #host: "localhost:5601"
+
+ # Kibana Space ID
+ # ID of the Kibana Space into which the dashboards should be loaded. By default,
+ # the Default Space will be used.
+ #space.id:
+
+# =============================== Elastic Cloud ================================
+
+# These settings simplify using Filebeat with the Elastic Cloud (https://cloud.elastic.co/).
+
+# The cloud.id setting overwrites the `output.elasticsearch.hosts` and
+# `setup.kibana.host` options.
+# You can find the `cloud.id` in the Elastic Cloud web UI.
+#cloud.id:
+
+# The cloud.auth setting overwrites the `output.elasticsearch.username` and
+# `output.elasticsearch.password` settings. The format is `:`.
+#cloud.auth:
+
+# ================================== Outputs ===================================
+
+# Configure what output to use when sending the data collected by the beat.
+
+# ---------------------------- Elasticsearch Output ----------------------------
+output.elasticsearch:
+ # Array of hosts to connect to.
+ # 直接到了 ElasticSearch
+ hosts: ["localhost:9200"]
+ # setup.ilm.enabled: false
+ setup.template.name: "filebeat"
+ setup.template.pattern: "filebeat-*"
+
+ # Protocol - either `http` (default) or `https`.
+ #protocol: "https"
+
+ # Authentication credentials - either API key or username/password.
+ #api_key: "id:api_key"
+ #username: "elastic"
+ #password: "changeme"
+
+# ------------------------------ Logstash Output -------------------------------
+# output.logstash:
+ # The Logstash hosts
+ # filebeat 没有运行在 docker 里面
+ # hosts: ["localhost:5044"]
+
+ # Optional SSL. By default is off.
+ # List of root certificates for HTTPS server verifications
+ #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]
+
+ # Certificate for SSL client authentication
+ #ssl.certificate: "/etc/pki/client/cert.pem"
+
+ # Client Certificate Key
+ #ssl.key: "/etc/pki/client/cert.key"
+
+# ================================= Processors =================================
+# processors:
+# - add_host_metadata:
+# when.not.contains.tags: forwarded
+# - add_cloud_metadata: ~
+# - add_docker_metadata: ~
+# - add_kubernetes_metadata: ~
+
+# ================================== Logging ===================================
+
+# Sets log level. The default log level is info.
+# Available log levels are: error, warning, info, debug
+#logging.level: debug
+
+# At debug level, you can selectively enable logging only for some components.
+# To enable all selectors use ["*"]. Examples of other selectors are "beat",
+# "publisher", "service".
+#logging.selectors: ["*"]
+
+# ============================= X-Pack Monitoring ==============================
+# Filebeat can export internal metrics to a central Elasticsearch monitoring
+# cluster. This requires xpack monitoring to be enabled in Elasticsearch. The
+# reporting is disabled by default.
+
+# Set to true to enable the monitoring reporter.
+#monitoring.enabled: false
+
+# Sets the UUID of the Elasticsearch cluster under which monitoring data for this
+# Filebeat instance will appear in the Stack Monitoring UI. If output.elasticsearch
+# is enabled, the UUID is derived from the Elasticsearch cluster referenced by output.elasticsearch.
+#monitoring.cluster_uuid:
+
+# Uncomment to send the metrics to Elasticsearch. Most settings from the
+# Elasticsearch output are accepted here as well.
+# Note that the settings should point to your Elasticsearch *monitoring* cluster.
+# Any setting that is not set is automatically inherited from the Elasticsearch
+# output configuration, so if you have the Elasticsearch output configured such
+# that it is pointing to your Elasticsearch monitoring cluster, you can simply
+# uncomment the following line.
+#monitoring.elasticsearch:
+
+# ============================== Instrumentation ===============================
+
+# Instrumentation support for the filebeat.
+#instrumentation:
+ # Set to true to enable instrumentation of filebeat.
+ #enabled: false
+
+ # Environment in which filebeat is running on (eg: staging, production, etc.)
+ #environment: ""
+
+ # APM Server hosts to report instrumentation results to.
+ #hosts:
+ # - http://localhost:8200
+
+ # API Key for the APM Server(s).
+ # If api_key is set then secret_token will be ignored.
+ #api_key:
+
+ # Secret token for the APM Server(s).
+ #secret_token:
+
+
+# ================================= Migration ==================================
+
+# This allows to enable 6.7 migration aliases
+#migration.6_to_7.enabled: true
+
diff --git a/webook/config/k8s.go b/webook/config/k8s.go
new file mode 100644
index 0000000000000000000000000000000000000000..3fbfb4f91f375366db6d722f7ac4ca3abdb8383e
--- /dev/null
+++ b/webook/config/k8s.go
@@ -0,0 +1,14 @@
+//go:build k8s
+
+// 使用 k8s 这个编译标签
+package config
+
+var Config = config{
+ DB: DBConfig{
+ // 本地连接
+ DSN: "root:root@tcp(webook-live-mysql:11309)/webook",
+ },
+ Redis: RedisConfig{
+ Addr: "webook-live-redis:11479",
+ },
+}
diff --git a/webook/config/logstash/logstash.config b/webook/config/logstash/logstash.config
new file mode 100644
index 0000000000000000000000000000000000000000..f43a33d6aae51611af4ccdca339c20de0b6e5f31
--- /dev/null
+++ b/webook/config/logstash/logstash.config
@@ -0,0 +1,21 @@
+input {
+ beats {
+ port => 5044
+ }
+}
+filter {
+ json {
+ source => "message"
+ target => "data"
+ }
+}
+
+output {
+ elasticsearch {
+ hosts => "elasticsearch:9200"
+ index => "logstash-%{+YYYY.MM.dd}"
+ }
+ stdout {
+ codec => rubydebug
+ }
+}
diff --git a/webook/config/logstash/logstash_no_filebeat.config b/webook/config/logstash/logstash_no_filebeat.config
new file mode 100644
index 0000000000000000000000000000000000000000..3efb3d4a9a301cd79ec39ceff2b36784226d8a6b
--- /dev/null
+++ b/webook/config/logstash/logstash_no_filebeat.config
@@ -0,0 +1,21 @@
+input {
+ file {
+ path => /usr/share/logstash/comment.log
+ }
+}
+filter {
+ json {
+ source => "message"
+ target => "data"
+ }
+}
+
+output {
+ elasticsearch {
+ hosts => "elasticsearch:9200"
+ index => "logstash-%{+YYYY.MM.dd}"
+ }
+ stdout {
+ codec => rubydebug
+ }
+}
diff --git a/webook/config/myjson.json b/webook/config/myjson.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/webook/config/types.go b/webook/config/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..786cc07bb22a7c24beec0eda334aa0136e39317c
--- /dev/null
+++ b/webook/config/types.go
@@ -0,0 +1,13 @@
+package config
+
+type config struct {
+ DB DBConfig
+ Redis RedisConfig
+}
+
+type DBConfig struct {
+ DSN string
+}
+type RedisConfig struct {
+ Addr string
+}
diff --git a/webook/di/doc.go b/webook/di/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd7c94024e60cc24022544ff0480bace514e5aa9
--- /dev/null
+++ b/webook/di/doc.go
@@ -0,0 +1,2 @@
+// Package di 是指 Dependency Inject(依赖注入)
+package di
diff --git a/webook/docker-compose.yaml b/webook/docker-compose.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4c9a8f4507b02fd851e1f09baa38287ddf562f81
--- /dev/null
+++ b/webook/docker-compose.yaml
@@ -0,0 +1,123 @@
+version: '3.0'
+services:
+ mysql8:
+ image: mysql:8.0
+ restart: always
+ command:
+# - 加入参数,设置 binlog 和主节点
+ - --default_authentication_plugin=mysql_native_password
+ - --binlog-format=ROW
+ - --server-id=1
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ volumes:
+ # 设置初始化脚本
+ - ./script/mysql/:/docker-entrypoint-initdb.d/
+ ports:
+ # 注意这里我映射为了 13316 端口
+ - "13316:3306"
+ redis:
+ image: 'bitnami/redis:7.2'
+ environment:
+ - ALLOW_EMPTY_PASSWORD=yes
+ ports:
+ - '6379:6379'
+ etcd:
+ image: 'bitnami/etcd:3.5.9'
+ environment:
+ - ALLOW_NONE_AUTHENTICATION=yes
+ ports:
+# 所以你要用 12379 端口来连接 etcd
+ - 12379:2379
+ mongo:
+ image: mongo:6.0
+ restart: always
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: example
+ ports:
+ - 27017:27017
+ prometheus:
+ image: prom/prometheus:v2.47.2
+ volumes:
+# - 将本地的 prometheus 文件映射到容器内的配置文件
+ - ./prometheus.yaml:/etc/prometheus/prometheus.yml
+ ports:
+# - 访问数据的端口
+ - 9090:9090
+ command:
+ - "--web.enable-remote-write-receiver"
+ - "--config.file=/etc/prometheus/prometheus.yml"
+ grafana:
+ image: grafana/grafana-enterprise:10.2.0
+ ports:
+ - 3000:3000
+ zipkin:
+# 用的是不支持 Kafka 之类的简化版本
+ image: openzipkin/zipkin-slim:2.24
+ ports:
+ - '9411:9411'
+
+ kafka:
+ image: 'bitnami/kafka:3.6.0'
+ ports:
+ - '9092:9092'
+ - '9094:9094'
+ environment:
+ - KAFKA_CFG_NODE_ID=0
+# - 三个分区
+ - KAFKA_CREATE_TOPICS=webook_binlog:3:1
+# - 允许自动创建 topic,线上不要开启
+ - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
+ - KAFKA_CFG_PROCESS_ROLES=controller,broker
+ - KAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://:9093,EXTERNAL://0.0.0.0:9094
+ - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094
+ - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
+ - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
+ - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
+
+ elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch:7.13.0
+ container_name: elasticsearch
+ environment:
+ - discovery.type=single-node
+ - "xpack.security.enabled=false"
+ - "ES_JAVA_OPTS=-Xms84m -Xmx512m"
+ ports:
+ - "9200:9200"
+
+ logstash:
+ image: docker.elastic.co/logstash/logstash:7.13.0
+ volumes:
+ - ./config/logstash:/usr/share/logstash/pipeline
+ # - ./logstash-logs:/usr/share/logstash/logs
+ # - ./app.log:/usr/share/logstash/app.log
+ - /var/log/comment.log:/usr/share/logstash/comment.log
+ environment:
+ - "xpack.monitoring.elasticsearch.hosts=http://elasticsearch:9200"
+ ports:
+ - 5044:5044
+
+ kibana:
+ # 注意检查你的 ElasticSearch 版本,这边我将 ES 也改到了这个版本
+ image: docker.elastic.co/kibana/kibana:7.13.0
+ environment:
+ - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
+ - i18n.locale=zh-CN
+ ports:
+ - "5601:5601"
+ canal:
+ image: canal/canal-server
+ environment:
+ - CANAL_IP=canal-server
+ - CANAL_PORT=11111
+ - CANAL_DESTINATIONS=example
+ depends_on:
+ - mysql8
+ - kafka
+ ports:
+# - 暴露了 canal 的端口,但是其实一般比较少直接跟 canal 打交道
+ - 11111:11111
+ volumes:
+ - ./script/canal/webook/instance.properties:/home/admin/canal-server/conf/webook/instance.properties
+ - ./script/canal/canal.properties:/home/admin/canal-server/conf/canal.properties
\ No newline at end of file
diff --git a/webook/feed/config/dev.yaml b/webook/feed/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..314daeeb12829e5d14954557c9366b29e7df4ed5
--- /dev/null
+++ b/webook/feed/config/dev.yaml
@@ -0,0 +1,20 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+grpc:
+ # 启动监听 9000 端口
+ server:
+ addr: ":8076"
+ etcdTTL: 60
+ client:
+ feed:
+ target: "etcd:///service/follow"
+
+redis:
+ addr: "localhost:6379"
+
+etcd:
+ endpoints:
+ - "localhost:12379"
+kafka:
+ addrs:
+ - "localhost:9094"
\ No newline at end of file
diff --git a/webook/feed/domain/extend_fields.go b/webook/feed/domain/extend_fields.go
new file mode 100644
index 0000000000000000000000000000000000000000..9660ce4a99a130a80c9d176c042de6104bdf8ac0
--- /dev/null
+++ b/webook/feed/domain/extend_fields.go
@@ -0,0 +1,21 @@
+package domain
+
+import (
+ "errors"
+ "fmt"
+ "github.com/ecodeclub/ekit"
+)
+
+type ExtendFields map[string]string
+
+var errKeyNotFound = errors.New("没有找到对应的 key")
+
+func (f ExtendFields) Get(key string) ekit.AnyValue {
+ val, ok := f[key]
+ if !ok {
+ return ekit.AnyValue{
+ Err: fmt.Errorf("%w, key %s", errKeyNotFound),
+ }
+ }
+ return ekit.AnyValue{Val: val}
+}
diff --git a/webook/feed/domain/feed.go b/webook/feed/domain/feed.go
new file mode 100644
index 0000000000000000000000000000000000000000..45cf5eeca8638bf448f46cb50678fb2d26dcd533
--- /dev/null
+++ b/webook/feed/domain/feed.go
@@ -0,0 +1,16 @@
+package domain
+
+import (
+ "time"
+)
+
+type FeedEvent struct {
+ ID int64
+ // 以 A 发表了一篇文章为例
+ // 如果是 Pull Event,也就是拉模型,那么 Uid 是 A 的id
+ // 如果是 Push Event,也就是推模型,那么 Uid 是 A 的某个粉丝的 id
+ Uid int64
+ Type string
+ Ctime time.Time
+ Ext ExtendFields
+}
diff --git a/webook/feed/events/artcle.go b/webook/feed/events/artcle.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb456539b0f4db28d396f59e63c875b7c17c0a62
--- /dev/null
+++ b/webook/feed/events/artcle.go
@@ -0,0 +1,69 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "strconv"
+ "time"
+)
+
+const topicArticleEvent = "article_feed_event"
+
+// ArticleFeedEvent 由业务方定义,本服务做适配
+type ArticleFeedEvent struct {
+ uid int64
+ aid int64
+}
+
+type ArticleEventConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+ svc service.FeedService
+}
+
+func NewArticleEventConsumer(
+ client sarama.Client,
+ l logger.LoggerV1,
+ svc service.FeedService) *ArticleEventConsumer {
+ ac := &ArticleEventConsumer{
+ svc: svc,
+ client: client,
+ l: l,
+ }
+ return ac
+}
+
+// Start 这边就是自己启动 goroutine 了
+func (r *ArticleEventConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("articleFeed",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{topicArticleEvent},
+ saramax.NewHandler[ArticleFeedEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+func (r *ArticleEventConsumer) Consume(msg *sarama.ConsumerMessage,
+ evt ArticleFeedEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return r.svc.CreateFeedEvent(ctx, domain.FeedEvent{
+ Type: service.FollowEventName,
+ Ext: map[string]string{
+ "uid": strconv.FormatInt(evt.uid, 10),
+ "aid": strconv.FormatInt(evt.uid, 10),
+ },
+ })
+
+}
diff --git a/webook/feed/events/base.go b/webook/feed/events/base.go
new file mode 100644
index 0000000000000000000000000000000000000000..a68482a82eaf2a341628c4caffcb101eea88c361
--- /dev/null
+++ b/webook/feed/events/base.go
@@ -0,0 +1,61 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+const topicFeedEvent = "feed_event"
+
+type FeedEvent struct {
+ Type string
+ Metadata map[string]string
+}
+
+type FeedEventConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+ svc service.FeedService
+}
+
+func NewFeedEventConsumer(
+ client sarama.Client,
+ l logger.LoggerV1,
+ svc service.FeedService) *FeedEventConsumer {
+ return &FeedEventConsumer{
+ svc: svc,
+ client: client,
+ l: l,
+ }
+}
+
+func (r *FeedEventConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("feed_event",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{topicFeedEvent},
+ saramax.NewHandler[FeedEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+func (r *FeedEventConsumer) Consume(msg *sarama.ConsumerMessage,
+ evt FeedEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return r.svc.CreateFeedEvent(ctx, domain.FeedEvent{
+ Type: evt.Type,
+ Ext: evt.Metadata,
+ })
+}
diff --git a/webook/feed/events/types.go b/webook/feed/events/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec4622544c2dc70b04f57c12375836b106c41d88
--- /dev/null
+++ b/webook/feed/events/types.go
@@ -0,0 +1,5 @@
+package events
+
+type Consumer interface {
+ Start() error
+}
diff --git a/webook/feed/grpc/feed.go b/webook/feed/grpc/feed.go
new file mode 100644
index 0000000000000000000000000000000000000000..f862f4f140a9b3fe582529a8b00b2b416fda4404
--- /dev/null
+++ b/webook/feed/grpc/feed.go
@@ -0,0 +1,66 @@
+package grpc
+
+import (
+ "context"
+ "encoding/json"
+ feedv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/feed/v1"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "google.golang.org/grpc"
+ "time"
+)
+
+type FeedEventGrpcSvc struct {
+ feedv1.UnimplementedFeedSvcServer
+ svc service.FeedService
+}
+
+func NewFeedEventGrpcSvc(svc service.FeedService) *FeedEventGrpcSvc {
+ return &FeedEventGrpcSvc{
+ svc: svc,
+ }
+}
+
+func (f *FeedEventGrpcSvc) Register(server grpc.ServiceRegistrar) {
+ feedv1.RegisterFeedSvcServer(server, f)
+}
+
+func (f *FeedEventGrpcSvc) CreateFeedEvent(ctx context.Context, request *feedv1.CreateFeedEventRequest) (*feedv1.CreateFeedEventResponse, error) {
+ err := f.svc.CreateFeedEvent(ctx, f.convertToDomain(request.GetFeedEvent()))
+ return &feedv1.CreateFeedEventResponse{}, err
+}
+
+func (f *FeedEventGrpcSvc) FindFeedEvents(ctx context.Context, request *feedv1.FindFeedEventsRequest) (*feedv1.FindFeedEventsResponse, error) {
+ eventList, err := f.svc.GetFeedEventList(ctx, request.GetUid(), request.Timestamp, request.Limit)
+ if err != nil {
+ return &feedv1.FindFeedEventsResponse{}, err
+ }
+ res := make([]*feedv1.FeedEvent, 0, len(eventList))
+ for _, event := range eventList {
+ res = append(res, f.convertToView(event))
+ }
+ return &feedv1.FindFeedEventsResponse{
+ FeedEvents: res,
+ }, nil
+}
+
+func (f *FeedEventGrpcSvc) convertToDomain(event *feedv1.FeedEvent) domain.FeedEvent {
+ ext := map[string]string{}
+ _ = json.Unmarshal([]byte(event.Content), &ext)
+ return domain.FeedEvent{
+ ID: event.Id,
+ Ctime: time.Unix(event.Ctime, 0),
+ Type: event.GetType(),
+ Ext: ext,
+ }
+}
+
+func (f *FeedEventGrpcSvc) convertToView(event domain.FeedEvent) *feedv1.FeedEvent {
+ val, _ := json.Marshal(event.Ext)
+ return &feedv1.FeedEvent{
+ Id: event.ID,
+ Type: event.Type,
+ Ctime: event.Ctime.Unix(),
+ Content: string(val),
+ }
+}
diff --git a/webook/feed/ioc/db.go b/webook/feed/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..2430c8e53dcb67bdd6a2fe22d6d8c14de8edda93
--- /dev/null
+++ b/webook/feed/ioc/db.go
@@ -0,0 +1,63 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ glogger "gorm.io/gorm/logger"
+ "gorm.io/plugin/opentelemetry/tracing"
+ "gorm.io/plugin/prometheus"
+)
+
+func InitDB(l logger.LoggerV1) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{
+ //使用 DEBUG 来打印
+ Logger: glogger.Default.LogMode(glogger.Info),
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // 接入 prometheus
+ err = db.Use(prometheus.New(prometheus.Config{
+ DBName: "webook",
+ // 每 15 秒采集一些数据
+ RefreshInterval: 15,
+ MetricsCollector: []prometheus.MetricsCollector{
+ &prometheus.MySQL{
+ VariableNames: []string{"Threads_running"},
+ },
+ }, // user defined metrics
+ }))
+ if err != nil {
+ panic(err)
+ }
+ err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics()))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
diff --git a/webook/feed/ioc/etcd.go b/webook/feed/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/feed/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/feed/ioc/follower_grpc.go b/webook/feed/ioc/follower_grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d84ae3fc2ed6bb56cb2726ab1802f7626fe48a5
--- /dev/null
+++ b/webook/feed/ioc/follower_grpc.go
@@ -0,0 +1,27 @@
+package ioc
+
+import (
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "github.com/spf13/viper"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+func InitFollowClient() followv1.FollowServiceClient {
+ type config struct {
+ Target string `yaml:"target"`
+ }
+ var cfg config
+ err := viper.UnmarshalKey("grpc.client.sms", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ conn, err := grpc.Dial(
+ cfg.Target,
+ grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ panic(err)
+ }
+ client := followv1.NewFollowServiceClient(conn)
+ return client
+}
diff --git a/webook/feed/ioc/grpc.go b/webook/feed/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..63ca1f1419d8cced7632927a7975d0fc63bf5853
--- /dev/null
+++ b/webook/feed/ioc/grpc.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/feed/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(l logger.LoggerV1,
+ ecli *clientv3.Client,
+ feedSvc *grpc2.FeedEventGrpcSvc) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ feedSvc.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "feed",
+ L: l,
+ EtcdTTL: cfg.EtcdTTL,
+ EtcdClient: ecli,
+ }
+}
diff --git a/webook/feed/ioc/handler.go b/webook/feed/ioc/handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..923b1c279ba435efecb52a6fdbecd84291b445d2
--- /dev/null
+++ b/webook/feed/ioc/handler.go
@@ -0,0 +1,18 @@
+package ioc
+
+import (
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+)
+
+func RegisterHandler(repo repository.FeedEventRepo, followClient followv1.FollowServiceClient) map[string]service.Handler {
+ articleHandler := service.NewArticleEventHandler(repo, followClient)
+ followHanlder := service.NewFollowEventHandler(repo)
+ likeHandler := service.NewLikeEventHandler(repo)
+ return map[string]service.Handler{
+ service.ArticleEventName: articleHandler,
+ service.FollowEventName: followHanlder,
+ service.LikeEventName: likeHandler,
+ }
+}
diff --git a/webook/feed/ioc/kafka.go b/webook/feed/ioc/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..c50e287c24aee321f2cda534e46341035c3a15c3
--- /dev/null
+++ b/webook/feed/ioc/kafka.go
@@ -0,0 +1,33 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/feed/events"
+ "github.com/IBM/sarama"
+ "github.com/spf13/viper"
+)
+
+func InitKafka() sarama.Client {
+ type Config struct {
+ Addrs []string `yaml:"addrs"`
+ }
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ var cfg Config
+ err := viper.UnmarshalKey("kafka", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := sarama.NewClient(cfg.Addrs, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+// NewConsumers 面临的问题依旧是所有的 Consumer 在这里注册一下
+func NewConsumers(article *events.ArticleEventConsumer, feed *events.FeedEventConsumer) []events.Consumer {
+ return []events.Consumer{
+ article,
+ feed,
+ }
+}
diff --git a/webook/feed/ioc/log.go b/webook/feed/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/feed/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/feed/ioc/redis.go b/webook/feed/ioc/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..0e987b628e361e9a43b048bc78c17b88d4bb31e0
--- /dev/null
+++ b/webook/feed/ioc/redis.go
@@ -0,0 +1,14 @@
+package ioc
+
+import (
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+)
+
+func InitRedis() redis.Cmdable {
+ // 这里演示读取特定的某个字段
+ cmd := redis.NewClient(&redis.Options{
+ Addr: viper.GetString("redis.addr"),
+ })
+ return cmd
+}
diff --git a/webook/feed/main.go b/webook/feed/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..6e66ce2304814027ed915339e19ef68cdf163f3d
--- /dev/null
+++ b/webook/feed/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/feed/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ for _, c := range app.consumers {
+ err := c.Start()
+ if err != nil {
+ panic(err)
+ }
+ }
+ err := app.server.Serve()
+ panic(err)
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+type App struct {
+ server *grpcx.Server
+ consumers []events.Consumer
+}
diff --git a/webook/feed/repository/cache/feed_event.go b/webook/feed/repository/cache/feed_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..61aa3a563ea731d22ac8ec39f587e30c5ab23d34
--- /dev/null
+++ b/webook/feed/repository/cache/feed_event.go
@@ -0,0 +1,56 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+var FolloweesNotFound = redis.Nil
+
+type FeedEventCache interface {
+ SetFollowees(ctx context.Context, follower int64, followees []int64) error
+ GetFollowees(ctx context.Context, follower int64) ([]int64, error)
+}
+
+type feedEventCache struct {
+ client redis.Cmdable
+}
+
+func NewFeedEventCache(client redis.Cmdable) FeedEventCache {
+ return &feedEventCache{
+ client: client,
+ }
+}
+
+const FolloweeKeyExpiration = 10 * time.Minute
+
+func (f *feedEventCache) SetFollowees(ctx context.Context, follower int64, followees []int64) error {
+ key := f.getFolloweeKey(follower)
+ followeesStr, err := json.Marshal(followees)
+ if err != nil {
+ return err
+ }
+ return f.client.Set(ctx, key, followeesStr, FolloweeKeyExpiration).Err()
+}
+
+func (f *feedEventCache) GetFollowees(ctx context.Context, follower int64) ([]int64, error) {
+ key := f.getFolloweeKey(follower)
+ res, err := f.client.Get(ctx, key).Result()
+ if errors.Is(err, redis.Nil) {
+ return nil, FolloweesNotFound
+ }
+ var followees []int64
+ err = json.Unmarshal([]byte(res), &followees)
+ if err != nil {
+ return nil, err
+ }
+ return followees, nil
+}
+
+func (f *feedEventCache) getFolloweeKey(follower int64) string {
+ return fmt.Sprintf("feed_event:%d", follower)
+}
diff --git a/webook/feed/repository/dao/ext_table.go b/webook/feed/repository/dao/ext_table.go
new file mode 100644
index 0000000000000000000000000000000000000000..3cf2818da1dae816874a67125f592cde3ba641c1
--- /dev/null
+++ b/webook/feed/repository/dao/ext_table.go
@@ -0,0 +1,5 @@
+package dao
+
+// LikeEvent 扩展表的机制
+type LikeEvent struct {
+}
diff --git a/webook/feed/repository/dao/feed_pull_event.go b/webook/feed/repository/dao/feed_pull_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad52020c83c4b2c83adb7b435f6fb034d08014ae
--- /dev/null
+++ b/webook/feed/repository/dao/feed_pull_event.go
@@ -0,0 +1,59 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+)
+
+// FeedPullEventDAO 拉模型
+type FeedPullEventDAO interface {
+ CreatePullEvent(ctx context.Context, event FeedPullEvent) error
+ FindPullEventList(ctx context.Context, uids []int64, timestamp, limit int64) ([]FeedPullEvent, error)
+ FindPullEventListWithTyp(ctx context.Context, typ string, uids []int64, timestamp, limit int64) ([]FeedPullEvent, error)
+}
+
+type FeedPullEvent struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ UID int64 `gorm:"column:uid;type:int(11);not null;"`
+ Type string `gorm:"column:type;type:varchar(255);comment:类型"`
+ Content string `gorm:"column:content;type:text;"`
+ // 发生时间
+ Ctime int64 `gorm:"column:ctime;comment:发生时间"`
+}
+
+type feedPullEventDAO struct {
+ db *gorm.DB
+}
+
+func NewFeedPullEventDAO(db *gorm.DB) FeedPullEventDAO {
+ return &feedPullEventDAO{
+ db: db,
+ }
+}
+
+func (f *feedPullEventDAO) FindPullEventListWithTyp(ctx context.Context, typ string, uids []int64, timestamp, limit int64) ([]FeedPullEvent, error) {
+ var events []FeedPullEvent
+ err := f.db.WithContext(ctx).
+ Where("uid in ?", uids).
+ Where("ctime < ?", timestamp).
+ Where("type = ?", typ).
+ Order("ctime desc").
+ Limit(int(limit)).
+ Find(&events).Error
+ return events, err
+}
+
+func (f *feedPullEventDAO) CreatePullEvent(ctx context.Context, event FeedPullEvent) error {
+ return f.db.WithContext(ctx).Create(&event).Error
+}
+
+func (f *feedPullEventDAO) FindPullEventList(ctx context.Context, uids []int64, timestamp, limit int64) ([]FeedPullEvent, error) {
+ var events []FeedPullEvent
+ err := f.db.WithContext(ctx).
+ Where("uid in ?", uids).
+ Where("ctime < ?", timestamp).
+ Order("ctime desc").
+ Limit(int(limit)).
+ Find(&events).Error
+ return events, err
+}
diff --git a/webook/feed/repository/dao/feed_push_event.go b/webook/feed/repository/dao/feed_push_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..3721231020459c5534561f565f522be413f5c62e
--- /dev/null
+++ b/webook/feed/repository/dao/feed_push_event.go
@@ -0,0 +1,59 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+)
+
+type FeedPushEventDAO interface {
+ // CreatePushEvents 创建推送事件
+ CreatePushEvents(ctx context.Context, events []FeedPushEvent) error
+ GetPushEvents(ctx context.Context, uid int64, timestamp, limit int64) ([]FeedPushEvent, error)
+ GetPushEventsWithTyp(ctx context.Context, typ string, uid int64, timestamp, limit int64) ([]FeedPushEvent, error)
+}
+
+type FeedPushEvent struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ UID int64 `gorm:"column:uid;type:int(11);not null;"`
+ Type string `gorm:"column:type;type:varchar(255);comment:类型"`
+ Content string `gorm:"column:content;type:text;"`
+ // 发生时间
+ Ctime int64 `gorm:"column:ctime;comment:发生时间"`
+}
+
+type feedPushEventDAO struct {
+ db *gorm.DB
+}
+
+func NewFeedPushEventDAO(db *gorm.DB) FeedPushEventDAO {
+ return &feedPushEventDAO{
+ db: db,
+ }
+}
+
+func (f *feedPushEventDAO) GetPushEventsWithTyp(ctx context.Context, typ string, uid int64, timestamp, limit int64) ([]FeedPushEvent, error) {
+ var events []FeedPushEvent
+ err := f.db.WithContext(ctx).
+ Where("uid = ?", uid).
+ Where("ctime < ?", timestamp).
+ Where("type = ?", typ).
+ Order("ctime desc").
+ Limit(int(limit)).
+ Find(&events).Error
+ return events, err
+}
+
+func (f *feedPushEventDAO) CreatePushEvents(ctx context.Context, events []FeedPushEvent) error {
+ return f.db.WithContext(ctx).Create(events).Error
+}
+
+func (f *feedPushEventDAO) GetPushEvents(ctx context.Context, uid int64, timestamp, limit int64) ([]FeedPushEvent, error) {
+ var events []FeedPushEvent
+ err := f.db.WithContext(ctx).
+ Where("uid = ?", uid).
+ Where("ctime < ?", timestamp).
+ Order("ctime desc").
+ Limit(int(limit)).
+ Find(&events).Error
+ return events, err
+}
diff --git a/webook/feed/repository/dao/init_tables.go b/webook/feed/repository/dao/init_tables.go
new file mode 100644
index 0000000000000000000000000000000000000000..9b63d2e7e2cb8f2e3c0306c8a734966cf306efda
--- /dev/null
+++ b/webook/feed/repository/dao/init_tables.go
@@ -0,0 +1,8 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(
+ &FeedPullEvent{}, &FeedPushEvent{})
+}
diff --git a/webook/feed/repository/feed_event.go b/webook/feed/repository/feed_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..4e4032f510d244f024cdb26d8745dba4dba97baf
--- /dev/null
+++ b/webook/feed/repository/feed_event.go
@@ -0,0 +1,161 @@
+package repository
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "time"
+)
+
+var FolloweesNotFound = cache.FolloweesNotFound
+
+type FeedEventRepo interface {
+ // CreatePushEvents 批量推事件
+ CreatePushEvents(ctx context.Context, events []domain.FeedEvent) error
+ // CreatePullEvent 创建拉事件
+ CreatePullEvent(ctx context.Context, event domain.FeedEvent) error
+ // FindPullEvents 获取拉事件,也就是关注的人发件箱里面的事件
+ FindPullEvents(ctx context.Context, uids []int64, timestamp, limit int64) ([]domain.FeedEvent, error)
+ // FindPushEvents 获取推事件,也就是自己收件箱里面的事件
+ FindPushEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error)
+ // FindPullEventsWithTyp 获取某个类型的拉事件,
+ FindPullEventsWithTyp(ctx context.Context, typ string, uids []int64, timestamp, limit int64) ([]domain.FeedEvent, error)
+ // FindPushEvents 获取某个类型的推事件,也就
+ FindPushEventsWithTyp(ctx context.Context, typ string, uid, timestamp, limit int64) ([]domain.FeedEvent, error)
+}
+
+type feedEventRepo struct {
+ pullDao dao.FeedPullEventDAO
+ pushDao dao.FeedPushEventDAO
+ feedCache cache.FeedEventCache
+}
+
+func NewFeedEventRepo(pullDao dao.FeedPullEventDAO, pushDao dao.FeedPushEventDAO, feedCache cache.FeedEventCache) FeedEventRepo {
+ return &feedEventRepo{
+ pullDao: pullDao,
+ pushDao: pushDao,
+ feedCache: feedCache,
+ }
+}
+
+func (f *feedEventRepo) FindPullEventsWithTyp(ctx context.Context, typ string, uids []int64, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ events, err := f.pullDao.FindPullEventListWithTyp(ctx, typ, uids, timestamp, limit)
+ if err != nil {
+ return nil, err
+ }
+ ans := make([]domain.FeedEvent, 0, len(events))
+ for _, e := range events {
+ ans = append(ans, convertToPullEventDomain(e))
+ }
+ return ans, nil
+}
+
+func (f *feedEventRepo) FindPushEventsWithTyp(ctx context.Context, typ string, uid, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ events, err := f.pushDao.GetPushEventsWithTyp(ctx, typ, uid, timestamp, limit)
+ if err != nil {
+ return nil, err
+ }
+ ans := make([]domain.FeedEvent, 0, len(events))
+ for _, e := range events {
+ ans = append(ans, convertToPushEventDomain(e))
+ }
+ return ans, nil
+}
+
+func (f *feedEventRepo) SetFollowees(ctx context.Context, follower int64, followees []int64) error {
+ return f.feedCache.SetFollowees(ctx, follower, followees)
+}
+
+func (f *feedEventRepo) GetFollowees(ctx context.Context, follower int64) ([]int64, error) {
+ followees, err := f.feedCache.GetFollowees(ctx, follower)
+ if errors.Is(err, cache.FolloweesNotFound) {
+ return nil, FolloweesNotFound
+ }
+ return followees, err
+}
+
+func (f *feedEventRepo) FindPullEvents(ctx context.Context, uids []int64, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ events, err := f.pullDao.FindPullEventList(ctx, uids, timestamp, limit)
+ if err != nil {
+ return nil, err
+ }
+ ans := make([]domain.FeedEvent, 0, len(events))
+ for _, e := range events {
+ ans = append(ans, convertToPullEventDomain(e))
+ }
+ return ans, nil
+}
+
+func (f *feedEventRepo) FindPushEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ events, err := f.pushDao.GetPushEvents(ctx, uid, timestamp, limit)
+ if err != nil {
+ return nil, err
+ }
+ ans := make([]domain.FeedEvent, 0, len(events))
+ for _, e := range events {
+ ans = append(ans, convertToPushEventDomain(e))
+ }
+ return ans, nil
+}
+
+func (f *feedEventRepo) CreatePushEvents(ctx context.Context, events []domain.FeedEvent) error {
+ pushEvents := make([]dao.FeedPushEvent, 0, len(events))
+ for _, e := range events {
+ pushEvents = append(pushEvents, convertToPushEventDao(e))
+ }
+ return f.pushDao.CreatePushEvents(ctx, pushEvents)
+}
+
+func (f *feedEventRepo) CreatePullEvent(ctx context.Context, event domain.FeedEvent) error {
+ return f.pullDao.CreatePullEvent(ctx, convertToPullEventDao(event))
+}
+
+func convertToPushEventDao(event domain.FeedEvent) dao.FeedPushEvent {
+ val, _ := json.Marshal(event.Ext)
+ return dao.FeedPushEvent{
+ Id: event.ID,
+ UID: event.Uid,
+ Type: event.Type,
+ Content: string(val),
+ Ctime: event.Ctime.Unix(),
+ }
+}
+
+func convertToPullEventDao(event domain.FeedEvent) dao.FeedPullEvent {
+ val, _ := json.Marshal(event.Ext)
+ return dao.FeedPullEvent{
+ Id: event.ID,
+ UID: event.Uid,
+ Type: event.Type,
+ Content: string(val),
+ Ctime: event.Ctime.Unix(),
+ }
+
+}
+
+func convertToPushEventDomain(event dao.FeedPushEvent) domain.FeedEvent {
+ var ext map[string]string
+ _ = json.Unmarshal([]byte(event.Content), &ext)
+ return domain.FeedEvent{
+ ID: event.Id,
+ Uid: event.UID,
+ Type: event.Type,
+ Ctime: time.Unix(event.Ctime, 0),
+ Ext: ext,
+ }
+}
+
+func convertToPullEventDomain(event dao.FeedPullEvent) domain.FeedEvent {
+ var ext map[string]string
+ _ = json.Unmarshal([]byte(event.Content), &ext)
+ return domain.FeedEvent{
+ ID: event.Id,
+ Uid: event.UID,
+ Type: event.Type,
+ Ctime: time.Unix(event.Ctime, 0),
+ Ext: ext,
+ }
+}
diff --git a/webook/feed/service/article_event.go b/webook/feed/service/article_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..314efe1853aa04e63b144a41e529749752d6914a
--- /dev/null
+++ b/webook/feed/service/article_event.go
@@ -0,0 +1,127 @@
+package service
+
+import (
+ "context"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "github.com/ecodeclub/ekit/slice"
+ "golang.org/x/sync/errgroup"
+ "sort"
+ "sync"
+ "time"
+)
+
+type ArticleEventHandler struct {
+ repo repository.FeedEventRepo
+ followClient followv1.FollowServiceClient
+}
+
+const (
+ ArticleEventName = "article_event"
+ threshold = 4
+ //threshold = 32
+)
+
+func NewArticleEventHandler(repo repository.FeedEventRepo, client followv1.FollowServiceClient) Handler {
+ return &ArticleEventHandler{
+ repo: repo,
+ followClient: client,
+ }
+}
+
+func (a *ArticleEventHandler) FindFeedEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ // 获取推模型事件
+ var (
+ eg errgroup.Group
+ mu sync.Mutex
+ )
+ events := make([]domain.FeedEvent, 0, limit*2)
+ // Push Event
+ eg.Go(func() error {
+ pushEvents, err := a.repo.FindPushEventsWithTyp(ctx, ArticleEventName, uid, timestamp, limit)
+ if err != nil {
+ return err
+ }
+ mu.Lock()
+ events = append(events, pushEvents...)
+ mu.Unlock()
+ return nil
+ })
+
+ // Pull Event
+ eg.Go(func() error {
+ resp, rerr := a.followClient.GetFollowee(ctx, &followv1.GetFolloweeRequest{
+ Follower: uid,
+ Offset: 0,
+ Limit: 200,
+ })
+ if rerr != nil {
+ return rerr
+ }
+ followeeIds := slice.Map(resp.FollowRelations, func(idx int, src *followv1.FollowRelation) int64 {
+ return src.Followee
+ })
+ pullEvents, err := a.repo.FindPullEventsWithTyp(ctx, ArticleEventName, followeeIds, timestamp, limit)
+ if err != nil {
+ return err
+ }
+ mu.Lock()
+ events = append(events, pullEvents...)
+ mu.Unlock()
+ return nil
+ })
+ err := eg.Wait()
+ if err != nil {
+ return nil, err
+ }
+ // 获取拉模型事件
+ // 获取默认的关注列表
+ sort.Slice(events, func(i, j int) bool {
+ return events[i].Ctime.Unix() > events[j].Ctime.Unix()
+ })
+
+ return events[:slice.Min[int]([]int{int(limit), len(events)})], nil
+}
+
+func (a *ArticleEventHandler) CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error {
+ uid, err := ext.Get("uid").AsInt64()
+ if err != nil {
+ return err
+ }
+ // 根据粉丝数判断使用推模型还是拉模型
+ resp, err := a.followClient.GetFollowStatic(ctx, &followv1.GetFollowStaticRequest{
+ Followee: uid,
+ })
+ if err != nil {
+ return err
+ }
+ // 粉丝数超出阈值使用拉模型
+ if resp.FollowStatic.Followers > threshold {
+ return a.repo.CreatePullEvent(ctx, domain.FeedEvent{
+ Uid: uid,
+ Type: ArticleEventName,
+ Ctime: time.Now(),
+ Ext: ext,
+ })
+ } else {
+ // 使用推模型
+ // 获取粉丝
+ fresp, err := a.followClient.GetFollower(ctx, &followv1.GetFollowerRequest{
+ Followee: uid,
+ })
+ if err != nil {
+ return err
+ }
+ events := make([]domain.FeedEvent, 0, len(fresp.FollowRelations))
+ for _, r := range fresp.GetFollowRelations() {
+ events = append(events, domain.FeedEvent{
+ Uid: r.Follower,
+ Type: ArticleEventName,
+ Ctime: time.Now(),
+ Ext: ext,
+ })
+ }
+ return a.repo.CreatePushEvents(ctx, events)
+ }
+}
diff --git a/webook/feed/service/client/follow_health_client.go b/webook/feed/service/client/follow_health_client.go
new file mode 100644
index 0000000000000000000000000000000000000000..98fef525d262838f26350507d8d5883869d792a7
--- /dev/null
+++ b/webook/feed/service/client/follow_health_client.go
@@ -0,0 +1,36 @@
+package client
+
+import (
+ "context"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+ "sync/atomic"
+)
+
+type FollowClient struct {
+ // 这个是真实的 RPC 客户端
+ followv1.FollowServiceClient
+
+ downgrade *atomic.Bool
+}
+
+func (f *FollowClient) GetFollowee(ctx context.Context, in *followv1.GetFolloweeRequest, opts ...grpc.CallOption) (resp *followv1.GetFolloweeResponse, err error) {
+ if f.downgrade.Load() {
+ // 或者返回特定的 error
+ return nil, nil
+ }
+ defer func() {
+ // 比如说这个,限流
+ if status.Code(err) == codes.Unavailable {
+ f.downgrade.Store(true)
+ // 这边呢?
+ go func() {
+ // 发心跳给 follow 检测,尝试退出 downgrade 状态
+ }()
+ }
+ }()
+ resp, err = f.FollowServiceClient.GetFollowee(ctx, in)
+ return
+}
diff --git a/webook/feed/service/feed.go b/webook/feed/service/feed.go
new file mode 100644
index 0000000000000000000000000000000000000000..41b1cb17b7a599d9acc2013ec30539b0955d542d
--- /dev/null
+++ b/webook/feed/service/feed.go
@@ -0,0 +1,134 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "github.com/ecodeclub/ekit/slice"
+ "golang.org/x/sync/errgroup"
+ "sort"
+ "sync"
+)
+
+type feedService struct {
+ repo repository.FeedEventRepo
+ handlerMap map[string]Handler
+ //followClient followv1.FollowServiceClient
+}
+
+func NewFeedService(repo repository.FeedEventRepo,
+ //client followv1.FollowServiceClient,
+ handlerMap map[string]Handler) FeedService {
+ return &feedService{
+ repo: repo,
+ // 你可以注入那个 health client
+ //followClient: client,
+ handlerMap: handlerMap,
+ }
+}
+
+func (f *feedService) registerService(typ string, handler Handler) {
+ f.handlerMap[typ] = handler
+}
+
+func (f *feedService) CreateFeedEvent(ctx context.Context, feed domain.FeedEvent) error {
+ // 需要可以解决的handler
+ handler, ok := f.handlerMap[feed.Type]
+ if !ok {
+ // 这里你可以考虑引入一个兜底的处理机制。
+ // 例如说在找不到的时候就默认丢过去 PushEvent 里面
+ // 对于大部分业务来说,都是合适的
+ return fmt.Errorf("未找到具体的业务处理逻辑 %s", feed.Type)
+ }
+ return handler.CreateFeedEvent(ctx, feed.Ext)
+}
+
+// GetFeedEventListV1 不依赖于 Handler 的直接查询
+// service 层面上的统一实现
+// 基本思路就是,收件箱查一下,发件箱查一下,合并结果(排序,分页),返回结果。
+// 按照时间戳倒序排序
+// 查询的时候,业务上不要做特殊处理
+//func (f *feedService) GetFeedEventListV1(ctx context.Context,
+// uid int64, timestamp, limit int64) ([]domain.FeedEvent, error) {
+// var (
+// eg errgroup.Group
+// pushEvents []domain.FeedEvent
+// pullEvents []domain.FeedEvent
+// )
+// eg.Go(func() error {
+//
+// // 性能瓶颈大概率出现在这里
+// // 你可以考虑说,在触发了降级的时候,或者 follow 本身触发了降级的时候
+// // 不走这个分支
+// // 我怎么知道 follow 降级了呢?
+//
+// // 在这边,pull event 你要获得你关注的所有人的 id
+// resp, err := f.followClient.GetFollowee(ctx, &followv1.GetFolloweeRequest{
+// // 你的 ID,为了获得你关注的所有人
+// Follower: uid,
+// // 可以把全部取过来
+// Limit: 100000,
+// // 你把时间戳过去,只查询[时间戳 - 1 天,时间戳]
+// })
+// if err != nil {
+// return err
+// }
+// uids := slice.Map(resp.FollowRelations, func(idx int, src *followv1.FollowRelation) int64 {
+// return src.Followee
+// })
+// pullEvents, err = f.repo.FindPullEvents(ctx, uids, timestamp, limit)
+// return err
+// })
+// eg.Go(func() error {
+// var err error
+// // 只有一次本地数据库查询,非常快
+// pushEvents, err = f.repo.FindPushEvents(ctx, uid, timestamp, limit)
+// return err
+// })
+// err := eg.Wait()
+// if err != nil {
+// return nil, err
+// }
+// events := append(pushEvents, pullEvents...)
+// // 这边你要再次排序
+// sort.Slice(events, func(i, j int) bool {
+// return events[i].Ctime.After(events[j].Ctime)
+// })
+// // 要小心不够数量。就是你想取10 条。结果总共才查到了 8 条
+// // min 这个方法在高版本 GO 里面才有
+// // slice.Min
+// return events[:min[int](len(events), int(limit))], nil
+//}
+
+func (f *feedService) GetFeedEventList(ctx context.Context, uid int64, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ // 万一,我有一部分业务有自己的查询逻辑;我另外一些业务没有特殊的查询逻辑
+ // 怎么写代码?
+ // 要注意尽可能减少数据库查询次数,和 follow client 的调用次数
+ var eg errgroup.Group
+ res := make([]domain.FeedEvent, 0, limit*int64(len(f.handlerMap)))
+ var mu sync.RWMutex
+ for _, handler := range f.handlerMap {
+ // 每一个业务方你都查一遍
+ // 要小心这一步,不要忘了
+ h := handler
+ eg.Go(func() error {
+ events, err := h.FindFeedEvents(ctx, uid, timestamp, limit)
+ if err != nil {
+ return err
+ }
+ mu.Lock()
+ res = append(res, events...)
+ mu.Unlock()
+ return nil
+ })
+ }
+ if err := eg.Wait(); err != nil {
+ return nil, err
+ }
+ // 聚合排序,难免的
+ sort.Slice(res, func(i, j int) bool {
+ return res[i].Ctime.Unix() > res[j].Ctime.Unix()
+ })
+ return res[:slice.Min[int]([]int{int(limit), len(res)})], nil
+}
diff --git a/webook/feed/service/follow_event.go b/webook/feed/service/follow_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..6053c8e090a35372d66a072784123c0306991659
--- /dev/null
+++ b/webook/feed/service/follow_event.go
@@ -0,0 +1,43 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "time"
+)
+
+const (
+ FollowEventName = "follow_event"
+)
+
+type FollowEventHandler struct {
+ repo repository.FeedEventRepo
+}
+
+func NewFollowEventHandler(repo repository.FeedEventRepo) Handler {
+ return &FollowEventHandler{
+ repo: repo,
+ }
+}
+
+func (f *FollowEventHandler) FindFeedEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ return f.repo.FindPushEventsWithTyp(ctx, FollowEventName, uid, timestamp, limit)
+}
+
+// CreateFeedEvent 创建跟随方式
+// 如果 A 关注了 B,那么
+// follower 就是 A
+// followee 就是 B
+func (f *FollowEventHandler) CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error {
+ followee, err := ext.Get("followee").AsInt64()
+ if err != nil {
+ return err
+ }
+ return f.repo.CreatePushEvents(ctx, []domain.FeedEvent{{
+ Uid: followee,
+ Type: FollowEventName,
+ Ctime: time.Now(),
+ Ext: ext,
+ }})
+}
diff --git a/webook/feed/service/like_event.go b/webook/feed/service/like_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..3fd2beb8629eb19c1bce9233437f169d60c32de7
--- /dev/null
+++ b/webook/feed/service/like_event.go
@@ -0,0 +1,53 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "time"
+)
+
+const (
+ LikeEventName = "like_event"
+)
+
+type LikeEventHandler struct {
+ repo repository.FeedEventRepo
+}
+
+func NewLikeEventHandler(repo repository.FeedEventRepo) Handler {
+ return &LikeEventHandler{
+ repo: repo,
+ }
+}
+
+func (l *LikeEventHandler) FindFeedEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) {
+ // 如果你有扩展表的机制
+ // 在这里查。你的 repository LikeEventRepository
+ // 如果要是你在数据库存储的时候,没有冗余用户的昵称
+ // BFF(你的业务方) 又不愿意去聚合(调用用户服务获得昵称)
+ // 就得你在这里查
+ return l.repo.FindPushEventsWithTyp(ctx, LikeEventName, uid, timestamp, limit)
+}
+
+// CreateFeedEvent 中的 ext 里面至少需要三个 id
+// liked int64: 被点赞的人
+// liker int64:点赞的人
+// bizId int64: 被点赞的东西
+// biz: string
+func (l *LikeEventHandler) CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error {
+ liked, err := ext.Get("liked").AsInt64()
+ if err != nil {
+ return err
+ }
+ // 你可以考虑校验其它数据
+ // 如果你用的是扩展表设计,那么这里就会调用自己业务的扩展表来存储数据
+ // 如果你希望冗余存储数据,但是业务方又不愿意存,
+ // 那么你在这里可以考虑回查业务获得一些数据
+ return l.repo.CreatePushEvents(ctx, []domain.FeedEvent{{
+ Uid: liked,
+ Type: LikeEventName,
+ Ctime: time.Now(),
+ Ext: ext,
+ }})
+}
diff --git a/webook/feed/service/types.go b/webook/feed/service/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..818951c5b4e95873b91bc65563ee0f73a688390d
--- /dev/null
+++ b/webook/feed/service/types.go
@@ -0,0 +1,17 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+)
+
+type FeedService interface {
+ CreateFeedEvent(ctx context.Context, feed domain.FeedEvent) error
+ GetFeedEventList(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error)
+}
+
+// Handler 具体业务处理逻辑
+type Handler interface {
+ CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error
+ FindFeedEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error)
+}
diff --git a/webook/feed/test/base.go b/webook/feed/test/base.go
new file mode 100644
index 0000000000000000000000000000000000000000..5fa4a4e3ffbbf8ce6fcc5e108a1315e01892c930
--- /dev/null
+++ b/webook/feed/test/base.go
@@ -0,0 +1,49 @@
+package test
+
+import "encoding/json"
+
+type EventCheck interface {
+ Check(want string, actual string) (any, any)
+}
+
+type ArticleEvent struct {
+ Uid string `json:"uid"`
+ Aid string `json:"aid"`
+ Title string `json:"title"`
+}
+
+func (a ArticleEvent) Check(want string, actual string) (any, any) {
+ wantVal := ArticleEvent{}
+ json.Unmarshal([]byte(want), &wantVal)
+ actualVal := ArticleEvent{}
+ json.Unmarshal([]byte(actual), &actualVal)
+ return wantVal, actualVal
+}
+
+type LikeEvent struct {
+ Liked string `json:"liked"`
+ Liker string `json:"liker"`
+ BizID string `json:"bizId"`
+ Biz string `json:"biz"`
+}
+
+func (l LikeEvent) Check(want string, actual string) (any, any) {
+ wantVal := LikeEvent{}
+ json.Unmarshal([]byte(want), &wantVal)
+ actualVal := LikeEvent{}
+ json.Unmarshal([]byte(actual), &actualVal)
+ return wantVal, actualVal
+}
+
+type FollowEvent struct {
+ Followee string `json:"followee"`
+ Follower string `json:"follower"`
+}
+
+func (f FollowEvent) Check(want string, actual string) (any, any) {
+ wantVal := FollowEvent{}
+ json.Unmarshal([]byte(want), &wantVal)
+ actualVal := FollowEvent{}
+ json.Unmarshal([]byte(actual), &actualVal)
+ return wantVal, actualVal
+}
diff --git a/webook/feed/test/config.yaml b/webook/feed/test/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a488ee6272930c97504e9c93431c60eea684f74f
--- /dev/null
+++ b/webook/feed/test/config.yaml
@@ -0,0 +1,22 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+grpc:
+ # 启动监听 9000 端口
+ server:
+ port: 8076
+ etcdAddr: "localhost:12376"
+ etcdTTL: 60
+ client:
+ feed:
+ target: "etcd:///service/feed"
+redis:
+ addr: "localhost:6379"
+kafka:
+ addrs:
+ - "localhost:9094"
+etcd:
+ endpoints:
+ - "localhost:12379"
+
+service:
+ threshold: 4
diff --git a/webook/feed/test/feed_test.go b/webook/feed/test/feed_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e35758acb0e79f934bc26f6068b0c4166f868ae4
--- /dev/null
+++ b/webook/feed/test/feed_test.go
@@ -0,0 +1,408 @@
+package test
+
+import (
+ "context"
+ "encoding/json"
+ feedv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/feed/v1"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ followv1Mock "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1/mocks"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/mock/gomock"
+ "strconv"
+ "testing"
+ "time"
+)
+
+// 测试主流程,创建推事件,创建拉事件
+// 运行的时候要注意工作目录要定位到当前目录
+type FeedTestSuite struct {
+ suite.Suite
+}
+
+func (f *FeedTestSuite) SetupSuite() {
+ // 初始化配置文件
+ viper.SetConfigFile("config.yaml")
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (f *FeedTestSuite) Test_Feed() {
+ // 初始化
+ server, mockFollowClient, db := InitGrpcServer(f.T())
+ defer func() {
+ db.Table("feed_push_events").Where("id > ? ", 0).Delete(&dao.FeedPushEvent{})
+ db.Table("feed_pull_events").Where("id > ? ", 0).Delete(&dao.FeedPullEvent{})
+ }()
+ // 设置followmock的值
+ ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Minute)
+ defer cancel()
+ // 创建事件
+ err := f.setupEvent(ctx, mockFollowClient, server)
+ require.NoError(f.T(), err)
+ // 获取feed流事件
+ wantEvents := f.getFeedEventWant(ctx, mockFollowClient, server)
+ resp, err := server.FindFeedEvents(ctx, &feedv1.FindFeedEventsRequest{
+ Uid: 1,
+ Limit: 20,
+ Timestamp: time.Now().Unix() + 3,
+ })
+ require.NoError(f.T(), err)
+ assert.Equal(f.T(), len(wantEvents), len(resp.FeedEvents))
+ checkerMap := map[string]EventCheck{
+ service.ArticleEventName: ArticleEvent{},
+ service.LikeEventName: LikeEvent{},
+ service.FollowEventName: FollowEvent{},
+ }
+ for i := 0; i < len(wantEvents); i++ {
+ wantEvent, actualEvent := wantEvents[i], resp.FeedEvents[i]
+ checker := checkerMap[wantEvent.Type]
+ wantContent, actualContent := checker.Check(wantEvent.Content, actualEvent.Content)
+ assert.Equal(f.T(), wantContent, actualContent)
+ }
+}
+
+func (f *FeedTestSuite) setupEvent(ctx context.Context, mockFollowClient *followv1Mock.MockFollowServiceClient, server feedv1.FeedSvcServer) error {
+ // 发表文章事件:用户2发表了四篇文章,用户3发表了3篇文章
+ articleEvents := []ArticleEvent{
+ {
+ Uid: "2",
+ Aid: "1",
+ Title: "用户2发表了文章1",
+ },
+ {
+ Uid: "2",
+ Aid: "2",
+ Title: "用户2发表了文章2",
+ },
+ {
+ Uid: "2",
+ Aid: "3",
+ Title: "用户2发表了文章3",
+ },
+ {
+ Uid: "2",
+ Aid: "4",
+ Title: "用户2发表了文章4",
+ },
+ }
+ mockFollowClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: 2,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 5,
+ },
+ }, nil).Times(len(articleEvents))
+
+ for _, event := range articleEvents {
+ content, _ := json.Marshal(event)
+ // 保证事件顺序
+ time.Sleep(1 * time.Second)
+ _, err := server.CreateFeedEvent(ctx, &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.ArticleEventName,
+ Content: string(content),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ articleEvents = []ArticleEvent{
+ {
+ Uid: "3",
+ Aid: "5",
+ Title: "用户3发表了文章5",
+ },
+ {
+ Uid: "3",
+ Aid: "6",
+ Title: "用户3发表了文章6",
+ },
+ {
+ Uid: "3",
+ Aid: "7",
+ Title: "用户3发表了文章7",
+ },
+ }
+ mockFollowClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: 3,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 2,
+ },
+ }, nil).Times(len(articleEvents))
+
+ mockFollowClient.EXPECT().GetFollower(gomock.Any(), &followv1.GetFollowerRequest{
+ Followee: 3,
+ }).Return(&followv1.GetFollowerResponse{
+ FollowRelations: []*followv1.FollowRelation{
+ {
+ Id: 6,
+ Follower: 1,
+ Followee: 3,
+ },
+ {
+ Id: 7,
+ Follower: 4,
+ Followee: 3,
+ },
+ },
+ }, nil).AnyTimes()
+ for _, event := range articleEvents {
+ content, _ := json.Marshal(event)
+ // 保证事件顺序
+ time.Sleep(1 * time.Second)
+ _, err := server.CreateFeedEvent(ctx, &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.ArticleEventName,
+ Content: string(content),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ // 创建点赞事件
+ likeEvents := []LikeEvent{
+ {
+ Liked: "1",
+ Liker: "10",
+ BizID: "8",
+ Biz: "article",
+ },
+ {
+ Liked: "1",
+ BizID: "9",
+ Biz: "article",
+ Liker: "11",
+ },
+ {
+ Liked: "1",
+ BizID: "10",
+ Biz: "article",
+ Liker: "12",
+ },
+ }
+ for _, event := range likeEvents {
+ content, _ := json.Marshal(event)
+ time.Sleep(1 * time.Second)
+ _, err := server.CreateFeedEvent(ctx, &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.LikeEventName,
+ Content: string(content),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ }
+ // 创建关注事件
+ followEvents := []FollowEvent{
+ {
+ Followee: "1",
+ Follower: "2",
+ },
+ {
+ Followee: "1",
+ Follower: "3",
+ },
+ {
+ Followee: "1",
+ Follower: "4",
+ },
+ }
+
+ for _, event := range followEvents {
+ content, _ := json.Marshal(event)
+ time.Sleep(1 * time.Second)
+ _, err := server.CreateFeedEvent(ctx, &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.FollowEventName,
+ Content: string(content),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (f *FeedTestSuite) getFeedEventWant(ctx context.Context, mockFollowClient *followv1Mock.MockFollowServiceClient, server feedv1.FeedSvcServer) []*feedv1.FeedEvent {
+ mockFollowClient.EXPECT().GetFollowee(gomock.Any(), &followv1.GetFolloweeRequest{
+ Follower: 1,
+ Offset: 0,
+ Limit: 200,
+ }).Return(&followv1.GetFolloweeResponse{
+ FollowRelations: []*followv1.FollowRelation{
+ {
+ Id: 1,
+ Follower: 1,
+ Followee: 2,
+ },
+ {
+ Id: 6,
+ Follower: 1,
+ Followee: 3,
+ },
+ {
+ Id: 8,
+ Follower: 1,
+ Followee: 4,
+ },
+ {
+ Id: 9,
+ Follower: 1,
+ Followee: 5,
+ },
+ {
+ Id: 10,
+ Follower: 1,
+ Followee: 6,
+ },
+ },
+ }, nil).AnyTimes()
+ wantArtcleEvents1 := []ArticleEvent{
+ {
+ Uid: "2",
+ Aid: "1",
+ Title: "用户2发表了文章1",
+ },
+ {
+ Uid: "2",
+ Aid: "2",
+ Title: "用户2发表了文章2",
+ },
+ {
+ Uid: "2",
+ Aid: "3",
+ Title: "用户2发表了文章3",
+ },
+ {
+ Uid: "2",
+ Aid: "4",
+ Title: "用户2发表了文章4",
+ },
+ }
+ wantArtcleEvents2 := []ArticleEvent{
+ {
+ Uid: "3",
+ Aid: "5",
+ Title: "用户3发表了文章5",
+ },
+ {
+ Uid: "3",
+ Aid: "6",
+ Title: "用户3发表了文章6",
+ },
+ {
+ Uid: "3",
+ Aid: "7",
+ Title: "用户3发表了文章7",
+ },
+ }
+ wantLikeEvents := []LikeEvent{
+ {
+ Liked: "1",
+ Liker: "10",
+ BizID: "8",
+ Biz: "article",
+ },
+ {
+ Liked: "1",
+ BizID: "9",
+ Biz: "article",
+ Liker: "11",
+ },
+ {
+ Liked: "1",
+ BizID: "10",
+ Biz: "article",
+ Liker: "12",
+ },
+ }
+ wantFollowEvents := []FollowEvent{
+ {
+ Followee: "1",
+ Follower: "2",
+ },
+ {
+ Followee: "1",
+ Follower: "3",
+ },
+ {
+ Followee: "1",
+ Follower: "4",
+ },
+ }
+ events := make([]*feedv1.FeedEvent, 0, 32)
+ for i := len(wantFollowEvents) - 1; i >= 0; i-- {
+ e := wantFollowEvents[i]
+ content, _ := json.Marshal(e)
+ events = append(events, &feedv1.FeedEvent{
+ User: &feedv1.User{
+ Id: 1,
+ },
+ Type: service.FollowEventName,
+ Content: string(content),
+ })
+ }
+ for i := len(wantLikeEvents) - 1; i >= 0; i-- {
+ e := wantLikeEvents[i]
+ content, _ := json.Marshal(e)
+ events = append(events, &feedv1.FeedEvent{
+ User: &feedv1.User{
+ Id: 1,
+ },
+ Type: service.LikeEventName,
+ Content: string(content),
+ })
+ }
+ for i := len(wantArtcleEvents2) - 1; i >= 0; i-- {
+ e := wantArtcleEvents2[i]
+ content, _ := json.Marshal(e)
+ events = append(events, &feedv1.FeedEvent{
+ User: &feedv1.User{
+ Id: 1,
+ },
+ Type: service.ArticleEventName,
+ Content: string(content),
+ })
+ }
+ for i := len(wantArtcleEvents1) - 1; i >= 0; i-- {
+ e := wantArtcleEvents1[i]
+ content, _ := json.Marshal(e)
+ uid, _ := strconv.ParseInt(e.Uid, 10, 64)
+ events = append(events, &feedv1.FeedEvent{
+ User: &feedv1.User{
+ Id: uid,
+ },
+ Type: service.ArticleEventName,
+ Content: string(content),
+ })
+ }
+
+ return events
+}
+
+func removeIdAndCtime(events []*feedv1.FeedEvent) []*feedv1.FeedEvent {
+ for _, e := range events {
+ e.Id = 0
+ e.Ctime = 0
+ }
+ return events
+}
+
+func TestFeedTestSuite(t *testing.T) {
+ suite.Run(t, new(FeedTestSuite))
+}
diff --git a/webook/feed/test/init.go b/webook/feed/test/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec39a44e3580f5449eb616f02af9297b22afc86f
--- /dev/null
+++ b/webook/feed/test/init.go
@@ -0,0 +1,31 @@
+package test
+
+import (
+ feedv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/feed/v1"
+ followMocks "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1/mocks"
+ "gitee.com/geekbang/basic-go/webook/feed/grpc"
+ "gitee.com/geekbang/basic-go/webook/feed/ioc"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "go.uber.org/mock/gomock"
+ "gorm.io/gorm"
+ "testing"
+)
+
+func InitGrpcServer(t *testing.T) (feedv1.FeedSvcServer, *followMocks.MockFollowServiceClient, *gorm.DB) {
+ loggerV1 := ioc.InitLogger()
+ db := ioc.InitDB(loggerV1)
+ feedPullEventDAO := dao.NewFeedPullEventDAO(db)
+ feedPushEventDAO := dao.NewFeedPushEventDAO(db)
+ cmdable := ioc.InitRedis()
+ feedEventCache := cache.NewFeedEventCache(cmdable)
+ feedEventRepo := repository.NewFeedEventRepo(feedPullEventDAO, feedPushEventDAO, feedEventCache)
+ mockCtrl := gomock.NewController(t)
+ followClient := followMocks.NewMockFollowServiceClient(mockCtrl)
+ v := ioc.RegisterHandler(feedEventRepo, followClient)
+ feedService := service.NewFeedService(feedEventRepo, v)
+ feedEventGrpcSvc := grpc.NewFeedEventGrpcSvc(feedService)
+ return feedEventGrpcSvc, followClient, db
+}
diff --git a/webook/feed/test/stress_test/config.yaml b/webook/feed/test/stress_test/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d8e4f59a0c0da007308b7f29b48f21cc9217bfc7
--- /dev/null
+++ b/webook/feed/test/stress_test/config.yaml
@@ -0,0 +1,23 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+grpc:
+ # 启动监听 9000 端口
+ server:
+ port: 8076
+ etcdAddr: "localhost:12376"
+ etcdTTL: 60
+ client:
+ feed:
+ target: "etcd:///service/feed"
+redis:
+ addr: "localhost:6379"
+kafka:
+ addrs:
+ - "localhost:9094"
+etcd:
+ endpoints:
+ - "localhost:12379"
+
+service:
+ threshold: 100000
+
diff --git a/webook/feed/test/stress_test/data_test.go b/webook/feed/test/stress_test/data_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..381dc470b98f29a020594a621177c9a707f033e0
--- /dev/null
+++ b/webook/feed/test/stress_test/data_test.go
@@ -0,0 +1,215 @@
+package stress_test
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ feedv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/feed/v1"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ followMocks "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1/mocks"
+ "gitee.com/geekbang/basic-go/webook/feed/ioc"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "gitee.com/geekbang/basic-go/webook/feed/test"
+ "gitee.com/geekbang/basic-go/webook/feed/test/stress_test/web"
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+ "math/rand"
+ "testing"
+ "time"
+)
+
+// 生成拉事件
+func generatePullEvent(mockFollowClient *followMocks.MockFollowServiceClient, id int64) test.ArticleEvent {
+ mockFollowClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: id,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 1000,
+ },
+ }, nil)
+ return test.ArticleEvent{
+ Uid: fmt.Sprintf("%d", id),
+ Aid: fmt.Sprintf("%d", time.Now().UnixNano()),
+ Title: fmt.Sprintf("%d发布了文章", id),
+ }
+}
+
+// 生成推事件
+func generatePushEvent(mockFollowClient *followMocks.MockFollowServiceClient, id, i int64) test.ArticleEvent {
+ // 生成几个推事件都包含id i
+ mockFollowClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: id + i,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 2,
+ },
+ }, nil)
+ mockFollowClient.EXPECT().GetFollower(gomock.Any(), &followv1.GetFollowerRequest{
+ Followee: id + i,
+ }).Return(&followv1.GetFollowerResponse{
+ FollowRelations: []*followv1.FollowRelation{
+ {
+ Id: time.Now().UnixNano(),
+ Follower: id,
+ Followee: id + i,
+ },
+ {
+ Id: time.Now().UnixNano(),
+ Follower: id + i + 1,
+ Followee: id + i,
+ },
+ },
+ }, nil)
+ return test.ArticleEvent{
+ Uid: fmt.Sprintf("%d", id+i),
+ Aid: fmt.Sprintf("%d", time.Now().UnixNano()),
+ Title: fmt.Sprintf("%d发布了文章", id+i),
+ }
+}
+
+// 生成数据
+func Test_ADDFeed(t *testing.T) {
+ server, followClient, _ := test.InitGrpcServer(t)
+ //生成拉事件的压力测试的数据
+ for i := 2; i < 100000; i++ {
+ event := generatePullEvent(followClient, int64(i))
+ ext, _ := json.Marshal(event)
+ _, err := server.CreateFeedEvent(context.Background(), &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.ArticleEventName,
+ Content: string(ext),
+ },
+ })
+ require.NoError(t, err)
+ }
+
+ //生成推事件的压力测试数据
+ for i := 0; i < 100000; i++ {
+ event := generatePushEvent(followClient, int64(300001), int64(i))
+ ext, _ := json.Marshal(event)
+ _, err := server.CreateFeedEvent(context.Background(), &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.ArticleEventName,
+ Content: string(ext),
+ },
+ })
+ require.NoError(t, err)
+ }
+
+ //生成推拉事件的压力测试数据
+ //拉事件服用上面拉事件的测试数据
+ for i := 0; i < 100000; i++ {
+ event := generatePushEvent(followClient, int64(400001), int64(i))
+ ext, _ := json.Marshal(event)
+ _, err := server.CreateFeedEvent(context.Background(), &feedv1.CreateFeedEventRequest{
+ FeedEvent: &feedv1.FeedEvent{
+ Type: service.ArticleEventName,
+ Content: string(ext),
+ },
+ })
+ require.NoError(t, err)
+ }
+}
+
+// 启动测试web
+// 记得要把工作目录定位到这里
+// 懒得再写一个测试的 IOC 了
+// follow 用的是本地 mock,所以也就是这个测试是排除了 follow 本身性能的影响
+func Test_Feed(t *testing.T) {
+ viper.SetConfigFile("config.yaml")
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+ loggerV1 := ioc.InitLogger()
+ db := ioc.InitDB(loggerV1)
+ feedPullEventDAO := dao.NewFeedPullEventDAO(db)
+ feedPushEventDAO := dao.NewFeedPushEventDAO(db)
+ cmdable := ioc.InitRedis()
+ feedEventCache := cache.NewFeedEventCache(cmdable)
+ feedEventRepo := repository.NewFeedEventRepo(feedPullEventDAO, feedPushEventDAO, feedEventCache)
+ mockCtrl := gomock.NewController(t)
+ // 不想用 mock,你就用真实的 follow rpc client
+ // 我想要模拟降级怎么办,你在 follow 加上降级的逻辑
+ followClient := followMocks.NewMockFollowServiceClient(mockCtrl)
+ v := ioc.RegisterHandler(feedEventRepo, followClient)
+ feedService := service.NewFeedService(feedEventRepo, v)
+ engine := gin.Default()
+ handler := web.NewFeedHandler(feedService)
+ handler.RegisterRoutes(engine)
+ // 设置mock数据
+ // 设置关注列表的测试数据
+ followClient.EXPECT().GetFollowee(gomock.Any(), gomock.Any()).Return(&followv1.GetFolloweeResponse{
+ FollowRelations: getFollowRelation(1),
+ }, nil).AnyTimes()
+ // 设置粉丝列表的测试数据
+ // 扩散百人
+ followClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: 4,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 800,
+ },
+ }, nil).AnyTimes()
+ followClient.EXPECT().GetFollower(gomock.Any(), &followv1.GetFollowerRequest{
+ Followee: 4,
+ }).Return(&followv1.GetFollowerResponse{
+ FollowRelations: getFollowerRelation(4, 800),
+ }, nil).AnyTimes()
+ // 扩散千人
+ followClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: 5,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 5000,
+ },
+ }, nil).AnyTimes()
+ followClient.EXPECT().GetFollower(gomock.Any(), &followv1.GetFollowerRequest{
+ Followee: 5,
+ }).Return(&followv1.GetFollowerResponse{
+ FollowRelations: getFollowerRelation(5, 5000),
+ }, nil).AnyTimes()
+ // 扩散万人
+ followClient.EXPECT().GetFollowStatic(gomock.Any(), &followv1.GetFollowStaticRequest{
+ Followee: 6,
+ }).Return(&followv1.GetFollowStaticResponse{
+ FollowStatic: &followv1.FollowStatic{
+ Followers: 50000,
+ },
+ }, nil).AnyTimes()
+ followClient.EXPECT().GetFollower(gomock.Any(), &followv1.GetFollowerRequest{
+ Followee: 6,
+ }).Return(&followv1.GetFollowerResponse{
+ FollowRelations: getFollowerRelation(6, 10000),
+ }, nil).AnyTimes()
+ engine.Run("127.0.0.1:8088")
+}
+
+func getFollowRelation(id int64) []*followv1.FollowRelation {
+ relations := make([]*followv1.FollowRelation, 0, 100001)
+ random := rand.Intn(200) + 300
+ for i := random - 200; i < random; i++ {
+ relations = append(relations, &followv1.FollowRelation{
+ Follower: id,
+ Followee: int64(i),
+ })
+ }
+ return relations
+}
+
+func getFollowerRelation(id int64, number int) []*followv1.FollowRelation {
+ relations := make([]*followv1.FollowRelation, 0, 100001)
+ for i := 1; i < number+1; i++ {
+ relations = append(relations, &followv1.FollowRelation{
+ Followee: id,
+ Follower: int64(i),
+ })
+ }
+ return relations
+}
diff --git a/webook/feed/test/stress_test/img.png b/webook/feed/test/stress_test/img.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e04736baf971c1bb739a86f70031e5adb189446
Binary files /dev/null and b/webook/feed/test/stress_test/img.png differ
diff --git a/webook/feed/test/stress_test/img_1.png b/webook/feed/test/stress_test/img_1.png
new file mode 100644
index 0000000000000000000000000000000000000000..69c0af70118a953caa8450f37cc582f6c9b4b6f9
Binary files /dev/null and b/webook/feed/test/stress_test/img_1.png differ
diff --git a/webook/feed/test/stress_test/img_10.png b/webook/feed/test/stress_test/img_10.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed1196c5f6369804cfd44282186e6c189802713e
Binary files /dev/null and b/webook/feed/test/stress_test/img_10.png differ
diff --git a/webook/feed/test/stress_test/img_11.png b/webook/feed/test/stress_test/img_11.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d6176b3022280bf24706cf74b3075495d6c8c02
Binary files /dev/null and b/webook/feed/test/stress_test/img_11.png differ
diff --git a/webook/feed/test/stress_test/img_2.png b/webook/feed/test/stress_test/img_2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c73ab2f16a199a70a634294a8bc9b286b9504e87
Binary files /dev/null and b/webook/feed/test/stress_test/img_2.png differ
diff --git a/webook/feed/test/stress_test/img_3.png b/webook/feed/test/stress_test/img_3.png
new file mode 100644
index 0000000000000000000000000000000000000000..88343e647719c00bca8b541b23c1786dfaced566
Binary files /dev/null and b/webook/feed/test/stress_test/img_3.png differ
diff --git a/webook/feed/test/stress_test/img_4.png b/webook/feed/test/stress_test/img_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e33ca48ec9fb4adc91aa07a9ed672876dd4fb548
Binary files /dev/null and b/webook/feed/test/stress_test/img_4.png differ
diff --git a/webook/feed/test/stress_test/img_5.png b/webook/feed/test/stress_test/img_5.png
new file mode 100644
index 0000000000000000000000000000000000000000..1d31bcb72d64bce8a51fd8a6c0ece3c2cf946302
Binary files /dev/null and b/webook/feed/test/stress_test/img_5.png differ
diff --git a/webook/feed/test/stress_test/img_6.png b/webook/feed/test/stress_test/img_6.png
new file mode 100644
index 0000000000000000000000000000000000000000..985db04aefd766d872c2e10b2ef140aeb011b57e
Binary files /dev/null and b/webook/feed/test/stress_test/img_6.png differ
diff --git a/webook/feed/test/stress_test/img_7.png b/webook/feed/test/stress_test/img_7.png
new file mode 100644
index 0000000000000000000000000000000000000000..957f033ee8cf26ec6a9ae4b003218164af85781a
Binary files /dev/null and b/webook/feed/test/stress_test/img_7.png differ
diff --git a/webook/feed/test/stress_test/img_8.png b/webook/feed/test/stress_test/img_8.png
new file mode 100644
index 0000000000000000000000000000000000000000..318e63cb3af7e018bb08f10c272b24098a3e1698
Binary files /dev/null and b/webook/feed/test/stress_test/img_8.png differ
diff --git a/webook/feed/test/stress_test/img_9.png b/webook/feed/test/stress_test/img_9.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e22ce8d8566a9367b23089cbfed8a66462345f3
Binary files /dev/null and b/webook/feed/test/stress_test/img_9.png differ
diff --git a/webook/feed/test/stress_test/readme.md b/webook/feed/test/stress_test/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..c9ba95fcff34a4281c6f25cc90ffe2a8f43062f1
--- /dev/null
+++ b/webook/feed/test/stress_test/readme.md
@@ -0,0 +1,85 @@
+## 压测脚本
+
+1. 准备数据
+执行 data_test.go 下的 Test_ADDFeed
+
+2. 启动feed的测试服务器
+```
+ 运行 data_test.go 下的Test_Feed函数
+
+```
+3. 准备prometheus
+```
+修改docker-compose的配置文件
+ prometheus:
+ image: prom/prometheus:v2.47.2
+ volumes:
+# - 将本地的 prometheus 文件映射到容器内的配置文件
+ - ./prometheus.yaml:/etc/prometheus/prometheus.yml
+ ports:
+# - 访问数据的端口
+ - 9090:9090
+ command:
+ # 开启remote writer
+ # 高版本是默认开启的
+ - "--web.enable-remote-write-receiver"
+ - "--config.file=/etc/prometheus/prometheus.yml"
+```
+
+4. 准备grafana
+ datasource 选择上面的prometheus
+ 模版选择:https://grafana.com/grafana/dashboards/19665-k6-prometheus/
+5. 运行压测脚本
+```
+cd webook/feed/test/stress_test
+ # 声明k6运行结果投递到响应的prometheus上
+export K6_PROMETHEUS_RW_SERVER_URL="http://localhost:9090/api/v1/write"
+# 这个是可选的,指定上报哪些数据
+export K6_PROMETHEUS_RW_TREND_STATS="p(50),p(90),p(95),p(99),min,max"
+ #下载k6
+mac的下载方式 brew install k6
+# 运行部分从 push 取,部分从 pull 取的压测
+k6 run -o experimental-prometheus-rw stress_mixedevent_test.js
+# 去grafana上查看压测结果
+# 运行 完全从 push 里面取的压测
+k6 run -o experimental-prometheus-rw stress_pushevent_test.js
+# 去grafana上查看压测结果
+# 运行 完全从 pull 里面取的压测
+k6 run -o experimental-prometheus-rw stress_pullevent_test.js
+# 去grafana上查看压测结果
+# 运行写测试扩散百人
+k6 run -o experimental-prometheus-rw stress_add100_test.js
+# 去grafana上查看压测结果
+# 运行写测试扩散千人
+k6 run -o experimental-prometheus-rw stress_add1000_test.js
+# 去grafana上查看压测结果
+# 运行写测试扩散万人
+k6 run -o experimental-prometheus-rw stress_add10000_test.js
+# 去grafana上查看压测结果
+
+```
+6. 截图
+
+扩散万人的
+
+
+
+扩散千人
+
+
+
+扩散百人
+
+
+
+从pull事件中取数据
+
+
+
+从push事件中取数据
+
+
+
+从混合事件中取数据
+
+
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/stress_add10000_test.js b/webook/feed/test/stress_test/stress_add10000_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ce9c0a53c7cef8547ff20f527b28769c0b646ed
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_add10000_test.js
@@ -0,0 +1,21 @@
+import http from 'k6/http';
+export let options = {
+ duration: '10s',
+ vus: 100,
+ rpc: 20,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/add";
+ let jsonStr = '{"uid":"6","aid":"article456","title":"Example Title"}'
+ var payload = JSON.stringify({
+ typ: "article_event",
+ ext: jsonStr,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/stress_add1000_test.js b/webook/feed/test/stress_test/stress_add1000_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..f606e1c380c89a72a58217c117d8c8d1f4591ebf
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_add1000_test.js
@@ -0,0 +1,21 @@
+import http from 'k6/http';
+export let options = {
+ duration: '10s',
+ vus: 100,
+ rpc: 20,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/add";
+ let jsonStr = '{"uid":"5","aid":"article456","title":"Example Title"}'
+ var payload = JSON.stringify({
+ typ: "article_event",
+ ext: jsonStr,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/stress_add100_test.js b/webook/feed/test/stress_test/stress_add100_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ea78adcf50bdc11f1c45eeb831457845818bb8f
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_add100_test.js
@@ -0,0 +1,21 @@
+import http from 'k6/http';
+export let options = {
+ duration: '10s',
+ vus: 100,
+ rpc: 20,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/add";
+ let jsonStr = '{"uid":"4","aid":"article456","title":"Example Title"}'
+ var payload = JSON.stringify({
+ typ: "article_event",
+ ext: jsonStr,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/stress_mixedevent_test.js b/webook/feed/test/stress_test/stress_mixedevent_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b022a9cd0fe45fa93f59518ad20baa2c0287dfe
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_mixedevent_test.js
@@ -0,0 +1,25 @@
+import http from 'k6/http';
+// http.setTimeout(30000);
+export let options = {
+ // 执行时间
+ duration: '10s',
+ // 并发量
+ vus: 100,
+ // 每秒多少请求
+ rpc: 20,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/list";
+ var payload = JSON.stringify({
+ uid: 40001,
+ limit: 10,
+ timestamp: 1708748101,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/stress_pullevent_test.js b/webook/feed/test/stress_test/stress_pullevent_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..291bbf934cabec3a7b3af56b7e951b04b49e15e3
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_pullevent_test.js
@@ -0,0 +1,23 @@
+
+import http from 'k6/http';
+// http.setTimeout(30000);
+export let options = {
+ duration: '10s',
+ vus: 10,
+ rpc: 10,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/list";
+ var payload = JSON.stringify({
+ uid: 30001,
+ limit: 10,
+ timestamp: 1708748101,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
diff --git a/webook/feed/test/stress_test/stress_pushevent_test.js b/webook/feed/test/stress_test/stress_pushevent_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c50bfa74c485c4ebfecc54d143da30806a514bc
--- /dev/null
+++ b/webook/feed/test/stress_test/stress_pushevent_test.js
@@ -0,0 +1,23 @@
+
+import http from 'k6/http';
+// http.setTimeout(30000);
+export let options = {
+ duration: '10s',
+ vus: 10,
+ rpc: 10,
+};
+export default () => {
+ var url = "http://127.0.0.1:8088/feed/list";
+ var payload = JSON.stringify({
+ uid:1,
+ limit: 10,
+ timestamp: 1708748101,
+ });
+
+ var params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ http.post(url, payload, params);
+};
\ No newline at end of file
diff --git a/webook/feed/test/stress_test/web/feed.go b/webook/feed/test/stress_test/web/feed.go
new file mode 100644
index 0000000000000000000000000000000000000000..4a866bb56b18cbbd3c3b0ba6f6ec13299d9e3e01
--- /dev/null
+++ b/webook/feed/test/stress_test/web/feed.go
@@ -0,0 +1,67 @@
+package web
+
+import (
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/feed/domain"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "github.com/gin-gonic/gin"
+ "net/http"
+)
+
+// 为了压测
+type FeedHandler struct {
+ svc service.FeedService
+}
+
+func NewFeedHandler(svc service.FeedService) *FeedHandler {
+ return &FeedHandler{
+ svc: svc,
+ }
+}
+
+func (f *FeedHandler) RegisterRoutes(s *gin.Engine) {
+ g := s.Group("/feed")
+ g.POST("/list", f.FindFeedEventList)
+ g.POST("/add", f.CreateFeedEvent)
+
+}
+
+func (f *FeedHandler) FindFeedEventList(ctx *gin.Context) {
+ var req FindFeedEventReq
+ err := ctx.Bind(&req)
+ events, err := f.svc.GetFeedEventList(ctx, req.UID, req.Timestamp, req.Limit)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Code: 0,
+ Msg: "成功",
+ Data: events,
+ })
+}
+
+func (f *FeedHandler) CreateFeedEvent(ctx *gin.Context) {
+ var req CreateFeedEventReq
+ err := ctx.Bind(&req)
+ var ext map[string]string
+ err = json.Unmarshal([]byte(req.Ext), &ext)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ }
+ err = f.svc.CreateFeedEvent(ctx, domain.FeedEvent{
+ Type: req.Typ,
+ Ext: ext,
+ })
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ }
+}
diff --git a/webook/feed/test/stress_test/web/types.go b/webook/feed/test/stress_test/web/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..a578ab42e34691457d8d76cb6dc2ede981008a41
--- /dev/null
+++ b/webook/feed/test/stress_test/web/types.go
@@ -0,0 +1,16 @@
+package web
+
+import "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+
+type FindFeedEventReq struct {
+ UID int64 `json:"uid"`
+ Limit int64 `json:"limit"`
+ Timestamp int64 `json:"timestamp"`
+}
+
+type CreateFeedEventReq struct {
+ Typ string `json:"typ"`
+ Ext string `json:"ext"`
+}
+
+type Result = ginx.Result
diff --git a/webook/feed/wire.go b/webook/feed/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..bebbe09601841c7de0cb1543cf957017901f54ed
--- /dev/null
+++ b/webook/feed/wire.go
@@ -0,0 +1,46 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/feed/events"
+ "gitee.com/geekbang/basic-go/webook/feed/grpc"
+ "gitee.com/geekbang/basic-go/webook/feed/ioc"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewFeedPushEventDAO,
+ dao.NewFeedPullEventDAO,
+ cache.NewFeedEventCache,
+ repository.NewFeedEventRepo,
+)
+
+var thirdProvider = wire.NewSet(
+ ioc.InitEtcdClient,
+ ioc.InitLogger,
+ ioc.InitRedis,
+ ioc.InitKafka,
+ ioc.InitDB,
+ ioc.InitFollowClient,
+)
+
+func Init() *App {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ ioc.RegisterHandler,
+ service.NewFeedService,
+ grpc.NewFeedEventGrpcSvc,
+ events.NewArticleEventConsumer,
+ events.NewFeedEventConsumer,
+ ioc.InitGRPCxServer,
+ ioc.NewConsumers,
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/feed/wire_gen.go b/webook/feed/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..da72fbff5de8afd270ade73990e8ddaef5612e97
--- /dev/null
+++ b/webook/feed/wire_gen.go
@@ -0,0 +1,51 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/feed/events"
+ "gitee.com/geekbang/basic-go/webook/feed/grpc"
+ "gitee.com/geekbang/basic-go/webook/feed/ioc"
+ "gitee.com/geekbang/basic-go/webook/feed/repository"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/feed/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/feed/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func Init() *App {
+ loggerV1 := ioc.InitLogger()
+ client := ioc.InitEtcdClient()
+ db := ioc.InitDB(loggerV1)
+ feedPullEventDAO := dao.NewFeedPullEventDAO(db)
+ feedPushEventDAO := dao.NewFeedPushEventDAO(db)
+ cmdable := ioc.InitRedis()
+ feedEventCache := cache.NewFeedEventCache(cmdable)
+ feedEventRepo := repository.NewFeedEventRepo(feedPullEventDAO, feedPushEventDAO, feedEventCache)
+ followServiceClient := ioc.InitFollowClient()
+ v := ioc.RegisterHandler(feedEventRepo, followServiceClient)
+ feedService := service.NewFeedService(feedEventRepo, v)
+ feedEventGrpcSvc := grpc.NewFeedEventGrpcSvc(feedService)
+ server := ioc.InitGRPCxServer(loggerV1, client, feedEventGrpcSvc)
+ saramaClient := ioc.InitKafka()
+ articleEventConsumer := events.NewArticleEventConsumer(saramaClient, loggerV1, feedService)
+ feedEventConsumer := events.NewFeedEventConsumer(saramaClient, loggerV1, feedService)
+ v2 := ioc.NewConsumers(articleEventConsumer, feedEventConsumer)
+ app := &App{
+ server: server,
+ consumers: v2,
+ }
+ return app
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewFeedPushEventDAO, dao.NewFeedPullEventDAO, cache.NewFeedEventCache, repository.NewFeedEventRepo)
+
+var thirdProvider = wire.NewSet(ioc.InitEtcdClient, ioc.InitLogger, ioc.InitRedis, ioc.InitKafka, ioc.InitDB, ioc.InitFollowClient)
diff --git a/webook/follow/config/config.yaml b/webook/follow/config/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..361017eb8f1e44ed73e369b7eaee115f29d4c2b0
--- /dev/null
+++ b/webook/follow/config/config.yaml
@@ -0,0 +1,6 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+
+grpc:
+# 启动监听 8090 端口
+ addr: ":8092"
\ No newline at end of file
diff --git a/webook/follow/domain/followrelation.go b/webook/follow/domain/followrelation.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe693ccb6e57c168e371ae0b394274467c08543e
--- /dev/null
+++ b/webook/follow/domain/followrelation.go
@@ -0,0 +1,19 @@
+package domain
+
+// FollowRelation 关注数据
+type FollowRelation struct {
+ // 被关注的人
+ Followee int64
+ // 关注的人
+ Follower int64
+ // 根据你的业务需要,你可以在这里加字段
+ // 比如说备注啊,标签啊之类的
+ // Note string
+}
+
+type FollowStatics struct {
+ // 被多少人关注
+ Followers int64
+ // 自己关注了多少人
+ Followees int64
+}
diff --git a/webook/follow/grpc/follow.go b/webook/follow/grpc/follow.go
new file mode 100644
index 0000000000000000000000000000000000000000..6ad5aa2eb2f9bb4e62c4e4a3b53249e877209773
--- /dev/null
+++ b/webook/follow/grpc/follow.go
@@ -0,0 +1,65 @@
+package grpc
+
+import (
+ "context"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "gitee.com/geekbang/basic-go/webook/follow/domain"
+ "gitee.com/geekbang/basic-go/webook/follow/service"
+ "google.golang.org/grpc"
+)
+
+type FollowServiceServer struct {
+ followv1.UnimplementedFollowServiceServer
+ svc service.FollowRelationService
+}
+
+func NewFollowRelationServiceServer(svc service.FollowRelationService) *FollowServiceServer {
+ return &FollowServiceServer{
+ svc: svc,
+ }
+}
+
+func (f *FollowServiceServer) Register(server grpc.ServiceRegistrar) {
+ followv1.RegisterFollowServiceServer(server, f)
+}
+
+func (f *FollowServiceServer) GetFollowee(ctx context.Context, request *followv1.GetFolloweeRequest) (*followv1.GetFolloweeResponse, error) {
+ relationList, err := f.svc.GetFollowee(ctx, request.Follower, request.Offset, request.Limit)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]*followv1.FollowRelation, 0, len(relationList))
+ for _, relation := range relationList {
+ res = append(res, f.convertToView(relation))
+ }
+ return &followv1.GetFolloweeResponse{
+ FollowRelations: res,
+ }, nil
+}
+
+func (f *FollowServiceServer) FollowInfo(ctx context.Context, request *followv1.FollowInfoRequest) (*followv1.FollowInfoResponse, error) {
+ info, err := f.svc.FollowInfo(ctx, request.Follower, request.Followee)
+ if err != nil {
+ return nil, err
+ }
+ return &followv1.FollowInfoResponse{
+ FollowRelation: f.convertToView(info),
+ }, nil
+}
+
+func (f *FollowServiceServer) Follow(ctx context.Context, request *followv1.FollowRequest) (*followv1.FollowResponse, error) {
+ err := f.svc.Follow(ctx, request.Follower, request.Followee)
+ return &followv1.FollowResponse{}, err
+}
+
+func (f *FollowServiceServer) CancelFollow(ctx context.Context, request *followv1.CancelFollowRequest) (*followv1.CancelFollowResponse, error) {
+ err := f.svc.CancelFollow(ctx, request.Follower, request.Followee)
+ return &followv1.CancelFollowResponse{}, err
+}
+
+func (f *FollowServiceServer) convertToView(relation domain.FollowRelation) *followv1.FollowRelation {
+ return &followv1.FollowRelation{
+ Followee: relation.Followee,
+ Follower: relation.Follower,
+ }
+}
diff --git a/webook/follow/integration/follow_test.go b/webook/follow/integration/follow_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f882c34833b9df5fa9ad7ff6ae6cc1e886c4f9b0
--- /dev/null
+++ b/webook/follow/integration/follow_test.go
@@ -0,0 +1,188 @@
+package integration
+
+import (
+ "context"
+ followv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/follow/v1"
+ "gitee.com/geekbang/basic-go/webook/follow/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+ "testing"
+)
+
+type FollowRelationSuite struct {
+ suite.Suite
+ db *gorm.DB
+ rdb redis.Cmdable
+ server followv1.FollowServiceServer
+}
+
+func (s *FollowRelationSuite) SetupSuite() {
+ s.db = startup.InitTestDB()
+ s.rdb = startup.InitRedis()
+ s.server = startup.InitServer()
+}
+func (s *FollowRelationSuite) TearDownSuite() {
+ err := s.db.Where("id > ?", 0).Delete(&dao.FollowRelation{}).Error
+ require.NoError(s.T(), err)
+}
+
+func (s *FollowRelationSuite) TestFollowRelation_ADD() {
+ testcases := []struct {
+ name string
+ before func()
+ req *followv1.AddFollowRelationRequest
+ wantVal *followv1.FollowRelation
+ wantErr error
+ }{
+ {
+ name: "添加正常",
+ before: func() {
+ },
+ req: &followv1.AddFollowRelationRequest{
+ Followee: 1,
+ Follower: 2,
+ },
+ wantVal: &followv1.FollowRelation{
+ Followee: 1,
+ Follower: 2,
+ },
+ },
+ {
+ name: "关注关系重复",
+ before: func() {
+ _, err := s.server.AddFollowRelation(context.Background(), &followv1.AddFollowRelationRequest{
+ Followee: 2,
+ Follower: 1,
+ })
+ require.NoError(s.T(), err)
+ },
+ req: &followv1.AddFollowRelationRequest{
+ Followee: 2,
+ Follower: 1,
+ },
+ wantVal: &followv1.FollowRelation{
+ Followee: 2,
+ Follower: 1,
+ },
+ },
+ }
+ for _, tc := range testcases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before()
+ _, err := s.server.AddFollowRelation(context.Background(), tc.req)
+ assert.Equal(t, tc.wantErr, err)
+ if err != nil {
+ return
+ }
+ relation, err := s.GetFollowRelation(tc.req.Followee, tc.req.Follower)
+ require.NoError(t, err)
+ relation.Id = 0
+ assert.Equal(t, tc.wantVal, relation)
+ })
+ }
+}
+
+func (s *FollowRelationSuite) TestFollowRelation_List() {
+ testcases := []struct {
+ name string
+ req int64
+ before func()
+ wantVal []*followv1.FollowRelation
+ }{
+ {
+ name: "获取列表",
+ req: 3,
+ before: func() {
+ reqs := []*followv1.FollowRelation{
+ {
+ Followee: 3,
+ Follower: 1,
+ },
+ {
+ Followee: 3,
+ Follower: 2,
+ },
+ {
+ Followee: 4,
+ Follower: 3,
+ },
+ {
+ Followee: 3,
+ Follower: 9,
+ },
+ }
+ for _, req := range reqs {
+ _, err := s.server.AddFollowRelation(context.Background(), &followv1.AddFollowRelationRequest{
+ Followee: req.Followee,
+ Follower: req.Follower,
+ })
+ require.NoError(s.T(), err)
+ }
+ },
+ wantVal: []*followv1.FollowRelation{
+ {
+ Followee: 4,
+ Follower: 3,
+ },
+ },
+ },
+ }
+ for _, tc := range testcases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before()
+ resp, err := s.server.FollowRelationList(context.Background(), &followv1.FollowRelationListRequest{
+ Follower: tc.req,
+ Limit: 3,
+ Offset: 0,
+ })
+ for _, val := range resp.FollowRelations {
+ val.Id = 0
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tc.wantVal, resp.FollowRelations)
+ })
+
+ }
+}
+
+func (s *FollowRelationSuite) TestFollowRelation_Info() {
+ // 准备数据
+ t := s.T()
+ _, err := s.server.AddFollowRelation(context.Background(), &followv1.AddFollowRelationRequest{
+ Followee: 8,
+ Follower: 9,
+ })
+ require.NoError(t, err)
+ relation, err := s.GetFollowRelation(8, 9)
+ require.NoError(s.T(), err)
+ resp, err := s.server.FollowRelationInfo(context.Background(), &followv1.FollowRelationInfoRequest{
+ Follower: relation.Follower,
+ Followee: relation.Followee,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &followv1.FollowRelation{
+ Id: relation.Id,
+ Followee: 8,
+ Follower: 9,
+ }, resp.FollowRelation)
+
+}
+
+func (s *FollowRelationSuite) GetFollowRelation(followee, follower int64) (*followv1.FollowRelation, error) {
+ resp, err := s.server.FollowRelationInfo(context.Background(), &followv1.FollowRelationInfoRequest{
+ Follower: follower,
+ Followee: followee,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return resp.FollowRelation, nil
+}
+
+func TestFollowSuite(t *testing.T) {
+ suite.Run(t, new(FollowRelationSuite))
+}
diff --git a/webook/follow/integration/startup/db.go b/webook/follow/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..7fb55329b1835b5e2f2534cb85b1098436cb3cc7
--- /dev/null
+++ b/webook/follow/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/follow/integration/startup/wire.go b/webook/follow/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..b675afaf8019cd968da3482fdae750ab0bc32542
--- /dev/null
+++ b/webook/follow/integration/startup/wire.go
@@ -0,0 +1,26 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/follow/grpc"
+ "gitee.com/geekbang/basic-go/webook/follow/repository"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/follow/service"
+ "github.com/google/wire"
+)
+
+func InitServer() *grpc.FollowServiceServer {
+ wire.Build(
+ InitRedis,
+ InitLog,
+ InitTestDB,
+ dao.NewGORMFollowRelationDAO,
+ cache.NewRedisFollowCache,
+ repository.NewFollowRelationRepository,
+ service.NewFollowRelationService,
+ grpc.NewFollowRelationServiceServer,
+ )
+ return new(grpc.FollowServiceServer)
+}
diff --git a/webook/follow/integration/startup/wire_gen.go b/webook/follow/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..06ffeb019f44b03e1a2f3ae43c80ccb4a02d1260
--- /dev/null
+++ b/webook/follow/integration/startup/wire_gen.go
@@ -0,0 +1,29 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/follow/grpc"
+ "gitee.com/geekbang/basic-go/webook/follow/repository"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/follow/service"
+)
+
+// Injectors from wire.go:
+
+func InitServer() *grpc.FollowServiceServer {
+ gormDB := InitTestDB()
+ followRelationDao := dao.NewGORMFollowRelationDAO(gormDB)
+ cmdable := InitRedis()
+ followCache := cache.NewRedisFollowCache(cmdable)
+ loggerV1 := InitLog()
+ followRepository := repository.NewFollowRelationRepository(followRelationDao, followCache, loggerV1)
+ followRelationService := service.NewFollowRelationService(followRepository)
+ followServiceServer := grpc.NewFollowRelationServiceServer(followRelationService)
+ return followServiceServer
+}
diff --git a/webook/follow/integration/table_store_test.go b/webook/follow/integration/table_store_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c0bd0dbf0e0b4e602da0043fe2e58268877762e1
--- /dev/null
+++ b/webook/follow/integration/table_store_test.go
@@ -0,0 +1,107 @@
+package integration
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "os"
+ "testing"
+ "time"
+)
+
+type TableStoreDAOTestSuite struct {
+ suite.Suite
+ dao *dao.TableStoreFollowRelationDao
+ client *tablestore.TableStoreClient
+}
+
+func (s *TableStoreDAOTestSuite) SetupSuite() {
+ // 自己去阿里云申请一个体验装,然后自己体验一下
+ // 一堆坑
+ endpoint := os.Getenv("TS_ENDPOINT")
+ accessId := os.Getenv("TS_ACCESS_KEY_ID")
+ accessKeySecret := os.Getenv("TS_ACCESS_KEY_SECRET")
+ instanceName := os.Getenv("TS_INSTANCE_NAME")
+ s.client = tablestore.NewClient(endpoint, instanceName, accessId, accessKeySecret)
+ s.InitTable()
+ s.dao = dao.NewTableStoreDao(s.client)
+}
+
+func (s *TableStoreDAOTestSuite) TearDownSuite() {
+ _, err := s.client.DeleteTable(&tablestore.DeleteTableRequest{
+ TableName: dao.FollowRelationTableName,
+ })
+ require.NoError(s.T(), err)
+}
+
+func (s *TableStoreDAOTestSuite) TestAdd() {
+ now := time.Now().UnixMilli()
+ err := s.dao.CreateFollowRelation(context.Background(), dao.FollowRelation{
+ Followee: 12,
+ Follower: 13,
+ Status: dao.FollowRelationStatusActive,
+ Ctime: now,
+ Utime: now,
+ })
+ require.NoError(s.T(), err)
+}
+
+func (s *TableStoreDAOTestSuite) TestCntFollowee() {
+ now := time.Now().UnixMilli()
+ err := s.dao.CreateFollowRelation(context.Background(), dao.FollowRelation{
+ Followee: 22,
+ Follower: 23,
+ Status: dao.FollowRelationStatusActive,
+ Ctime: now,
+ Utime: now,
+ })
+ require.NoError(s.T(), err)
+ res, err := s.dao.CntFollowee(context.Background(), 23)
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), int64(1), res)
+ res, err = s.dao.CntFollower(context.Background(), 22)
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), int64(1), res)
+}
+
+func TestTableStoreDAO(t *testing.T) {
+ suite.Run(t, new(TableStoreDAOTestSuite))
+}
+
+func (s *TableStoreDAOTestSuite) InitTable() {
+ createTableRequest := new(tablestore.CreateTableRequest)
+
+ tableMeta := new(tablestore.TableMeta)
+ // 声明表名
+ tableMeta.TableName = dao.FollowRelationTableName
+
+ tableMeta.AddPrimaryKeyColumn("follower", tablestore.PrimaryKeyType_INTEGER)
+ tableMeta.AddPrimaryKeyColumn("followee", tablestore.PrimaryKeyType_INTEGER)
+
+ // 添加属性列 备注 创建时间 和更新时间
+ tableMeta.AddDefinedColumn("utime", tablestore.DefinedColumn_INTEGER)
+ tableMeta.AddDefinedColumn("ctime", tablestore.DefinedColumn_INTEGER)
+ tableMeta.AddDefinedColumn("status", tablestore.DefinedColumn_INTEGER)
+ tableOption := new(tablestore.TableOption)
+ // 数据的过期时间
+ tableOption.TimeToAlive = -1
+ tableOption.MaxVersion = 1
+ reservedThroughput := new(tablestore.ReservedThroughput)
+ reservedThroughput.Readcap = 0
+ reservedThroughput.Writecap = 0
+ createTableRequest.TableMeta = tableMeta
+ createTableRequest.TableOption = tableOption
+ createTableRequest.ReservedThroughput = reservedThroughput
+ _, err := s.client.CreateTable(createTableRequest)
+ if err != nil {
+ optsErr, ok := err.(*tablestore.OtsError)
+ if ok {
+ if optsErr.Code == "OTSObjectAlreadyExist" {
+ return
+ }
+ }
+ panic(err)
+ }
+}
diff --git a/webook/follow/ioc/db.go b/webook/follow/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..59bb23941266c9972a8651e245f1059a14d4aec2
--- /dev/null
+++ b/webook/follow/ioc/db.go
@@ -0,0 +1,64 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ //prometheus2 "gitee.com/geekbang/basic-go/webook/pkg/gormx/callbacks/prometheus"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ glogger "gorm.io/gorm/logger"
+ "gorm.io/plugin/opentelemetry/tracing"
+ "gorm.io/plugin/prometheus"
+)
+
+func InitDB(l logger.LoggerV1) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{
+ //使用 DEBUG 来打印
+ Logger: glogger.Default.LogMode(glogger.Info),
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // 接入 prometheus
+ err = db.Use(prometheus.New(prometheus.Config{
+ DBName: "webook",
+ // 每 15 秒采集一些数据
+ RefreshInterval: 15,
+ MetricsCollector: []prometheus.MetricsCollector{
+ &prometheus.MySQL{
+ VariableNames: []string{"Threads_running"},
+ },
+ }, // user defined metrics
+ }))
+ if err != nil {
+ panic(err)
+ }
+ err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics()))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
diff --git a/webook/follow/ioc/grpc.go b/webook/follow/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..2a2a9b4b7ecabc29e05e9e68bc5e7d3fdb7b0d2e
--- /dev/null
+++ b/webook/follow/ioc/grpc.go
@@ -0,0 +1,25 @@
+package ioc
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/follow/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "github.com/spf13/viper"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(followRelation *grpc2.FollowServiceServer) *grpcx.Server {
+ type Config struct {
+ Addr string `yaml:"addr"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ followRelation.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Addr: cfg.Addr,
+ }
+}
diff --git a/webook/follow/ioc/log.go b/webook/follow/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/follow/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/follow/main.go b/webook/follow/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..24c15957b528f086d0227b679eed4c136bc271c0
--- /dev/null
+++ b/webook/follow/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ err := app.server.Serve()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/config.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+type App struct {
+ server *grpcx.Server
+}
diff --git a/webook/follow/repository/cache/redis.go b/webook/follow/repository/cache/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8cfbff25c1729f3dfd992661998bdf48fab6a9c
--- /dev/null
+++ b/webook/follow/repository/cache/redis.go
@@ -0,0 +1,83 @@
+package cache
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/follow/domain"
+ "github.com/redis/go-redis/v9"
+ "strconv"
+)
+
+var ErrKeyNotExist = redis.Nil
+
+type RedisFollowCache struct {
+ client redis.Cmdable
+}
+
+const (
+ // 被多少人关注
+ fieldFollowerCnt = "follower_cnt"
+ // 关注了多少人
+ fieldFolloweeCnt = "followee_cnt"
+)
+
+func (r *RedisFollowCache) Follow(ctx context.Context, follower, followee int64) error {
+ return r.updateStaticsInfo(ctx, follower, followee, 1)
+}
+
+func (r *RedisFollowCache) CancelFollow(ctx context.Context, follower, followee int64) error {
+ return r.updateStaticsInfo(ctx, follower, followee, -1)
+}
+
+// 我现在要更新数量了
+// 这个地方必要根本没有必要非得保持一起成功或者一起失败
+func (r *RedisFollowCache) updateStaticsInfo(ctx context.Context, follower, followee int64, delta int64) error {
+ tx := r.client.TxPipeline()
+ // 理论上你应该要做到一起成功或者一起失败,
+ // 用 lua 脚本可以
+ // 首先更新 follower 的 followee 数量
+ // 我往这个 tx 里面增加了两个指令,Tx 只是记录了,还没发过去 redis 服务端
+ tx.HIncrBy(ctx, r.staticsKey(follower), fieldFolloweeCnt, delta)
+ // 其次你就要更新 followee 的 follower 的数量
+ tx.HIncrBy(ctx, r.staticsKey(followee), fieldFollowerCnt, delta)
+
+ // Exec 的时候,会把两条命令发过去 redis server 上,并且这两条命令会一起执行
+ // 中间不会有别的命令执行
+ // 问题来了,有没有可能,执行了第一条命令成功,但是没有执行第二条?
+ // Redis 的事务,不具备 ACID 的特性
+ _, err := tx.Exec(ctx)
+ return err
+}
+
+func (r *RedisFollowCache) StaticsInfo(ctx context.Context, uid int64) (domain.FollowStatics, error) {
+ data, err := r.client.HGetAll(ctx, r.staticsKey(uid)).Result()
+ if err != nil {
+ return domain.FollowStatics{}, err
+ }
+ // 也认为没有数据
+ if len(data) == 0 {
+ return domain.FollowStatics{}, ErrKeyNotExist
+ }
+ // 理论上来说,这里不可能有 error
+ followerCnt, _ := strconv.ParseInt(data[fieldFollowerCnt], 10, 64)
+ followeeCnt, _ := strconv.ParseInt(data[fieldFolloweeCnt], 10, 64)
+ return domain.FollowStatics{
+ Followees: followeeCnt,
+ Followers: followerCnt,
+ }, nil
+}
+
+func (r *RedisFollowCache) SetStaticsInfo(ctx context.Context, uid int64, statics domain.FollowStatics) error {
+ key := r.staticsKey(uid)
+ return r.client.HMSet(ctx, key, fieldFolloweeCnt, statics.Followees, fieldFollowerCnt, statics.Followers).Err()
+}
+
+func (r *RedisFollowCache) staticsKey(uid int64) string {
+ return fmt.Sprintf("follow:statics:%d", uid)
+}
+
+func NewRedisFollowCache(client redis.Cmdable) FollowCache {
+ return &RedisFollowCache{
+ client: client,
+ }
+}
diff --git a/webook/follow/repository/cache/types.go b/webook/follow/repository/cache/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..4ce2d7a51e504ce87cd417f97ce48c1fb05d8543
--- /dev/null
+++ b/webook/follow/repository/cache/types.go
@@ -0,0 +1,13 @@
+package cache
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/follow/domain"
+)
+
+type FollowCache interface {
+ StaticsInfo(ctx context.Context, uid int64) (domain.FollowStatics, error)
+ SetStaticsInfo(ctx context.Context, uid int64, statics domain.FollowStatics) error
+ Follow(ctx context.Context, follower, followee int64) error
+ CancelFollow(ctx context.Context, follower, followee int64) error
+}
diff --git a/webook/follow/repository/dao/gorm.go b/webook/follow/repository/dao/gorm.go
new file mode 100644
index 0000000000000000000000000000000000000000..208c63dfc8a7cec3760c496c4b298fadab13fe2d
--- /dev/null
+++ b/webook/follow/repository/dao/gorm.go
@@ -0,0 +1,83 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "time"
+)
+
+type GORMFollowRelationDAO struct {
+ db *gorm.DB
+}
+
+func (g *GORMFollowRelationDAO) CntFollower(ctx context.Context, uid int64) (int64, error) {
+ var res int64
+ err := g.db.WithContext(ctx).
+ Select("count(follower)").
+ // 我这个怎么办?
+ // 考虑在 followee 上创建一个索引(followee, status)
+ // 行不行?
+ Where("followee = ? AND status = ?",
+ uid, FollowRelationStatusActive).Count(&res).Error
+ return res, err
+}
+
+func (g *GORMFollowRelationDAO) CntFollowee(ctx context.Context, uid int64) (int64, error) {
+ var res int64
+ err := g.db.WithContext(ctx).
+ Select("count(followee)").
+ // 我在这里,能利用到 的联合唯一索引
+ Where("follower = ? AND status = ?",
+ uid, FollowRelationStatusActive).Count(&res).Error
+ return res, err
+}
+
+func (g *GORMFollowRelationDAO) UpdateStatus(ctx context.Context, followee int64, follower int64, status uint8) error {
+ now := time.Now().UnixMilli()
+ return g.db.WithContext(ctx).
+ Where("follower = ? AND followee = ?", follower, followee).
+ Updates(map[string]any{
+ "status": status,
+ "utime": now,
+ }).Error
+}
+
+func (g *GORMFollowRelationDAO) FollowRelationList(ctx context.Context,
+ follower, offset, limit int64) ([]FollowRelation, error) {
+ var res []FollowRelation
+ // 这样就达成了覆盖索引的效果
+ err := g.db.WithContext(ctx).Select("follower, followee").
+ // 这个查询要求我们要在 follower 上创建一个索引,或者 联合唯一索引
+ // 进一步考虑,将 status 也加入索引
+ Where("follower = ? AND status = ?", follower, FollowRelationStatusActive).
+ Offset(int(offset)).Limit(int(limit)).
+ Find(&res).Error
+ return res, err
+}
+
+func (g *GORMFollowRelationDAO) FollowRelationDetail(ctx context.Context, follower int64, followee int64) (FollowRelation, error) {
+ var res FollowRelation
+ err := g.db.WithContext(ctx).Where("follower = ? AND followee = ? AND status = ?",
+ follower, followee, FollowRelationStatusActive).First(&res).Error
+ return res, err
+}
+
+func (g *GORMFollowRelationDAO) CreateFollowRelation(ctx context.Context, f FollowRelation) error {
+ now := time.Now().UnixMilli()
+ f.Utime = now
+ f.Ctime = now
+ f.Status = FollowRelationStatusActive
+ return g.db.WithContext(ctx).Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "utime": now,
+ "status": FollowRelationStatusActive,
+ }),
+ }).Create(&f).Error
+}
+
+func NewGORMFollowRelationDAO(db *gorm.DB) FollowRelationDao {
+ return &GORMFollowRelationDAO{
+ db: db,
+ }
+}
diff --git a/webook/follow/repository/dao/init_tables.go b/webook/follow/repository/dao/init_tables.go
new file mode 100644
index 0000000000000000000000000000000000000000..afbf1fbccf45f0907ed246206d5561d69493984d
--- /dev/null
+++ b/webook/follow/repository/dao/init_tables.go
@@ -0,0 +1,7 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(&FollowRelation{})
+}
diff --git a/webook/follow/repository/dao/table_store.go b/webook/follow/repository/dao/table_store.go
new file mode 100644
index 0000000000000000000000000000000000000000..47b744397338e90d014d831145eb46d78c3fb00f
--- /dev/null
+++ b/webook/follow/repository/dao/table_store.go
@@ -0,0 +1,160 @@
+package dao
+
+import (
+ "context"
+ "fmt"
+ "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
+ "gorm.io/gorm"
+ "time"
+)
+
+const FollowRelationTableName = "follow_relations"
+
+var (
+ ErrFollowerNotFound = gorm.ErrRecordNotFound
+)
+
+type TableStoreFollowRelationDao struct {
+ client *tablestore.TableStoreClient
+}
+
+func (t *TableStoreFollowRelationDao) FollowRelationList(ctx context.Context, follower, offset, limit int64) ([]FollowRelation, error) {
+ request := &tablestore.SQLQueryRequest{
+ // 可以替换成 select *
+ // 这种写法有什么问题?有什么隐患?
+ // SQL 注入的隐患
+ // 在实践中,如果要利用前端的输入来拼接 SQL 语句,千万要小心 SQL 注入的问题
+ // select id,follower,followee from follow_relations where follower = 1 OR 1 = 1 AND status = 2 OFFSET 0 LIMIT 10
+ // select id,follower,followee from follow_relations where follower = 1; TRUNCATE users OR 1 = 1 AND status = 2 OFFSET 0 LIMIT 10
+ // 用户登录 select * from xxx where username = %s AND password = %s;
+ //Query: fmt.Sprintf("select id,follower,followee from %s where follower = %s AND status = %d OFFSET %d LIMIT %d",
+ // FollowRelationTableName, "1 OR 1 = 1", FollowRelationStatusActive, offset, limit)}
+ Query: fmt.Sprintf("select id,follower,followee from %s where follower = %d AND status = %d OFFSET %d LIMIT %d",
+ FollowRelationTableName, follower, FollowRelationStatusActive, offset, limit)}
+ // SELECT * FROM xx WHERE id = ? // 是利用占位符的,然后传参数
+ response, err := t.client.SQLQuery(request)
+ if err != nil {
+ return nil, err
+ }
+ resultSet := response.ResultSet
+ followRelations := make([]FollowRelation, 0, limit)
+ for resultSet.HasNext() {
+ row := resultSet.Next()
+ followRelation := FollowRelation{}
+ followRelation.ID, _ = row.GetInt64ByName("id")
+ followRelation.Follower, _ = row.GetInt64ByName("follower")
+ followRelation.Followee, _ = row.GetInt64ByName("followee")
+ followRelations = append(followRelations, followRelation)
+ }
+ return followRelations, nil
+}
+
+func (t *TableStoreFollowRelationDao) UpdateStatus(ctx context.Context, followee int64, follower int64, status uint8) error {
+ cond := tablestore.NewCompositeColumnCondition(tablestore.LO_AND)
+ // 更新条件,对标 WHERE 语句
+ // 多个 Filter 是 AND 条件连在一起
+ cond.AddFilter(tablestore.NewSingleColumnCondition("follower", tablestore.CT_EQUAL, follower))
+ cond.AddFilter(tablestore.NewSingleColumnCondition("followee", tablestore.CT_EQUAL, followee))
+ req := new(tablestore.UpdateRowChange)
+ req.TableName = FollowRelationTableName
+ // 我预期这一行数据是存在的
+ // 不在的话,会报错
+ req.SetCondition(tablestore.RowExistenceExpectation_EXPECT_EXIST)
+ req.SetColumnCondition(cond)
+ req.PutColumn("status", int64(status))
+ _, err := t.client.UpdateRow(&tablestore.UpdateRowRequest{
+ UpdateRowChange: req,
+ })
+ return err
+}
+
+func (t *TableStoreFollowRelationDao) CntFollower(ctx context.Context, uid int64) (int64, error) {
+ request := &tablestore.SQLQueryRequest{
+ Query: fmt.Sprintf("SELECT COUNT(follower) as cnt from %s where followee = %d AND status = %d",
+ FollowRelationTableName, uid, FollowRelationStatusActive)}
+ response, err := t.client.SQLQuery(request)
+ if err != nil {
+ return 0, err
+ }
+ resultSet := response.ResultSet
+ if resultSet.HasNext() {
+ row := resultSet.Next()
+ return row.GetInt64ByName("cnt")
+ }
+ return 0, ErrFollowerNotFound
+}
+
+func (t *TableStoreFollowRelationDao) CntFollowee(ctx context.Context, uid int64) (int64, error) {
+ request := &tablestore.SQLQueryRequest{
+ Query: fmt.Sprintf("SELECT COUNT(followee) as cnt from %s where follower = %d AND status = %d",
+ FollowRelationTableName, uid, FollowRelationStatusActive)}
+ response, err := t.client.SQLQuery(request)
+ if err != nil {
+ return 0, err
+ }
+ resultSet := response.ResultSet
+ if resultSet.HasNext() {
+ row := resultSet.Next()
+ return row.GetInt64ByName("cnt")
+ }
+ return 0, ErrFollowerNotFound
+}
+
+func (t *TableStoreFollowRelationDao) CreateFollowRelation(ctx context.Context, c FollowRelation) error {
+ now := time.Now().UnixMilli()
+ // UpdateRowRequest + RowExistenceExpectation_IGNORE
+ // 可以实现一个 insert or update 的语义
+ // 单纯的使用 update 或者 put,都不能达成这个效果
+ req := new(tablestore.UpdateRowRequest)
+ pk := &tablestore.PrimaryKey{}
+ pk.AddPrimaryKeyColumn("follower", c.Follower)
+ pk.AddPrimaryKeyColumn("followee", c.Followee)
+ change := &tablestore.UpdateRowChange{
+ TableName: FollowRelationTableName,
+ // 有一个小的问题,这边其实可以不用 id, 直接用 follower 和 followee 构成一个主键
+ // 如果要用 ID,你可以用自增主键
+ PrimaryKey: pk,
+ }
+ change.SetCondition(tablestore.RowExistenceExpectation_IGNORE)
+ // 只能用 Int64,
+ change.PutColumn("status", int64(c.Status))
+ change.PutColumn("ctime", now)
+ change.PutColumn("utime", now)
+ req.UpdateRowChange = change
+ _, err := t.client.UpdateRow(req)
+ return err
+}
+
+func (t *TableStoreFollowRelationDao) FollowRelationDetail(ctx context.Context, follower, followee int64) (FollowRelation, error) {
+ request := &tablestore.SQLQueryRequest{
+ Query: fmt.Sprintf("select id,follower,followee from %s where follower = %d AND followee = %d AND status = %d",
+ FollowRelationTableName, follower, followee, FollowRelationStatusActive)}
+ response, err := t.client.SQLQuery(request)
+ if err != nil {
+ return FollowRelation{}, err
+ }
+ resultSet := response.ResultSet
+ if resultSet.HasNext() {
+ row := resultSet.Next()
+ return t.rowToEntity(row), nil
+ }
+ return FollowRelation{}, ErrFollowerNotFound
+}
+
+func (t *TableStoreFollowRelationDao) rowToEntity(row tablestore.SQLRow) FollowRelation {
+ var res FollowRelation
+ res.ID, _ = row.GetInt64ByName("id")
+ res.Follower, _ = row.GetInt64ByName("follower")
+ res.Followee, _ = row.GetInt64ByName("followee")
+ status, _ := row.GetInt64ByName("status")
+ res.Status = uint8(status)
+ res.Ctime, _ = row.GetInt64ByName("ctime")
+ res.Utime, _ = row.GetInt64ByName("utime")
+ return res
+}
+
+func NewTableStoreDao(client *tablestore.TableStoreClient) *TableStoreFollowRelationDao {
+ return &TableStoreFollowRelationDao{
+ client: client,
+ }
+}
diff --git a/webook/follow/repository/dao/types.go b/webook/follow/repository/dao/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e1e1c825aa294c301472946dc5f2bbf3326af08
--- /dev/null
+++ b/webook/follow/repository/dao/types.go
@@ -0,0 +1,74 @@
+package dao
+
+import "context"
+
+// FollowRelation 这个是类似于点赞的表设计
+// 取消关注,不是真的删除了数据,而是更新了状态
+type FollowRelation struct {
+ ID int64 `gorm:"primaryKey,autoIncrement,column:id"`
+
+ // 在这里建一个联合唯一索引
+ // 注意列的顺序
+ // 考虑到我们的典型场景是,我关注了多少人 where follower = ? (传入 uid = 123)
+ // 所以应该是
+
+ // 如果我的典型场景是,我有多少粉丝 WHERE followee = ? (传入 uid = 123)
+ // 这种情况下 在后
+ Follower int64 `gorm:"type:int(11);not null;uniqueIndex:follower_followee"`
+ Followee int64 `gorm:"type:int(11);not null;uniqueIndex:follower_followee"`
+
+ // 对应于关注来说,就是插入或者将这个状态更新为可用状态
+ // 对于取消关注来说,就是将这个状态更新为不可用状态
+ Status uint8
+
+ // 这里你可以根据自己的业务来增加字段,比如说
+ // 关系类型,可以搞些什么普通关注,特殊关注
+ // Type int64 `gorm:"column:type;type:int(11);comment:关注类型 0-普通关注"`
+ // 备注
+ // Note string `gorm:"column:remark;type:varchar(255);"`
+ // 创建时间
+ Ctime int64
+ Utime int64
+}
+
+const (
+ FollowRelationStatusUnknown uint8 = iota
+ FollowRelationStatusActive
+ FollowRelationStatusInactive
+)
+
+type FollowRelationDao interface {
+ // FollowRelationList 获取某人的关注列表
+ FollowRelationList(ctx context.Context, follower, offset, limit int64) ([]FollowRelation, error)
+ FollowRelationDetail(ctx context.Context, follower int64, followee int64) (FollowRelation, error)
+ // CreateFollowRelation 创建联系人
+ CreateFollowRelation(ctx context.Context, c FollowRelation) error
+ // UpdateStatus 更新状态
+ UpdateStatus(ctx context.Context, followee int64, follower int64, status uint8) error
+ // CntFollower 统计计算关注自己的人有多少
+ CntFollower(ctx context.Context, uid int64) (int64, error)
+ // CntFollowee 统计自己关注了多少人
+ CntFollowee(ctx context.Context, uid int64) (int64, error)
+}
+
+// UserRelation 另外一种设计方案,但是不要这么做
+type UserRelation struct {
+ ID int64 `gorm:"primaryKey,autoIncrement,column:id"`
+ Uid1 int64 `gorm:"column:uid1;type:int(11);not null;uniqueIndex:user_contact_index"`
+ Uid2 int64 `gorm:"column:uid2;type:int(11);not null;uniqueIndex:user_contact_index"`
+ Block bool // 拉黑
+ Mute bool // 屏蔽
+ Follow bool // 关注
+}
+
+type FollowStatics struct {
+ ID int64 `gorm:"primaryKey,autoIncrement,column:id"`
+ Uid int64 `gorm:"unique"`
+ // 有多少粉丝
+ Followers int64
+ // 关注了多少人
+ Followees int64
+
+ Utime int64
+ Ctime int64
+}
diff --git a/webook/follow/repository/followrelation.go b/webook/follow/repository/followrelation.go
new file mode 100644
index 0000000000000000000000000000000000000000..5f67f78a5c9c59a54f792ebf80132688942d1a48
--- /dev/null
+++ b/webook/follow/repository/followrelation.go
@@ -0,0 +1,120 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/follow/domain"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+type FollowRepository interface {
+ // GetFollowee 获取某人的关注列表
+ GetFollowee(ctx context.Context, follower, offset, limit int64) ([]domain.FollowRelation, error)
+ // FollowInfo 查看关注人的详情
+ FollowInfo(ctx context.Context, follower int64, followee int64) (domain.FollowRelation, error)
+ // AddFollowRelation 创建关注关系
+ AddFollowRelation(ctx context.Context, f domain.FollowRelation) error
+ // InactiveFollowRelation 取消关注
+ InactiveFollowRelation(ctx context.Context, follower int64, followee int64) error
+ GetFollowStatics(ctx context.Context, uid int64) (domain.FollowStatics, error)
+}
+
+type CachedRelationRepository struct {
+ dao dao.FollowRelationDao
+ cache cache.FollowCache
+ l logger.LoggerV1
+}
+
+func (d *CachedRelationRepository) GetFollowStatics(ctx context.Context, uid int64) (domain.FollowStatics, error) {
+ // 快路径
+ res, err := d.cache.StaticsInfo(ctx, uid)
+ if err == nil {
+ return res, err
+ }
+ // 慢路径
+ res.Followers, err = d.dao.CntFollower(ctx, uid)
+ if err != nil {
+ return res, err
+ }
+ res.Followees, err = d.dao.CntFollowee(ctx, uid)
+ if err != nil {
+ return res, err
+ }
+ err = d.cache.SetStaticsInfo(ctx, uid, res)
+ if err != nil {
+ // 这里记录日志
+ d.l.Error("缓存关注统计信息失败",
+ logger.Error(err),
+ logger.Int64("uid", uid))
+ }
+ return res, nil
+}
+
+func (d *CachedRelationRepository) InactiveFollowRelation(ctx context.Context, follower int64, followee int64) error {
+ err := d.dao.UpdateStatus(ctx, followee, follower, dao.FollowRelationStatusInactive)
+ if err != nil {
+ return err
+ }
+ return d.cache.CancelFollow(ctx, follower, followee)
+}
+
+func (d *CachedRelationRepository) GetFollowee(ctx context.Context, follower, offset, limit int64) ([]domain.FollowRelation, error) {
+ // 你要做缓存,撑死了就是缓存第一页
+ // 缓存命中率贼低
+ followerList, err := d.dao.FollowRelationList(ctx, follower, offset, limit)
+ if err != nil {
+ return nil, err
+ }
+ return d.genFollowRelationList(followerList), nil
+}
+
+func (d *CachedRelationRepository) genFollowRelationList(followerList []dao.FollowRelation) []domain.FollowRelation {
+ res := make([]domain.FollowRelation, 0, len(followerList))
+ for _, c := range followerList {
+ res = append(res, d.toDomain(c))
+ }
+ return res
+}
+
+func (d *CachedRelationRepository) FollowInfo(ctx context.Context, follower int64, followee int64) (domain.FollowRelation, error) {
+ // 要比列表有缓存价值
+ c, err := d.dao.FollowRelationDetail(ctx, follower, followee)
+ if err != nil {
+ return domain.FollowRelation{}, err
+ }
+ return d.toDomain(c), nil
+}
+
+func (d *CachedRelationRepository) AddFollowRelation(ctx context.Context, c domain.FollowRelation) error {
+ err := d.dao.CreateFollowRelation(ctx, d.toEntity(c))
+ if err != nil {
+ return err
+ }
+ // 这里要更新在 Redis 上的缓存计数,对于 A 关注了 B 来说,这里要增加 A 的 followee 的数量
+ // 同时要增加 B 的 follower 的数量
+ return d.cache.Follow(ctx, c.Follower, c.Followee)
+}
+
+func (d *CachedRelationRepository) toDomain(fr dao.FollowRelation) domain.FollowRelation {
+ return domain.FollowRelation{
+ Followee: fr.Followee,
+ Follower: fr.Follower,
+ }
+}
+
+func (d *CachedRelationRepository) toEntity(c domain.FollowRelation) dao.FollowRelation {
+ return dao.FollowRelation{
+ Followee: c.Followee,
+ Follower: c.Follower,
+ }
+}
+
+func NewFollowRelationRepository(dao dao.FollowRelationDao,
+ cache cache.FollowCache, l logger.LoggerV1) FollowRepository {
+ return &CachedRelationRepository{
+ dao: dao,
+ cache: cache,
+ l: l,
+ }
+}
diff --git a/webook/follow/service/followrelation.go b/webook/follow/service/followrelation.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e4061d77e029e8bb3a53055431fa204ef2d24c1
--- /dev/null
+++ b/webook/follow/service/followrelation.go
@@ -0,0 +1,46 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/follow/domain"
+ "gitee.com/geekbang/basic-go/webook/follow/repository"
+)
+
+type FollowRelationService interface {
+ GetFollowee(ctx context.Context, follower, offset, limit int64) ([]domain.FollowRelation, error)
+ FollowInfo(ctx context.Context,
+ follower, followee int64) (domain.FollowRelation, error)
+ Follow(ctx context.Context, follower, followee int64) error
+ CancelFollow(ctx context.Context, follower, followee int64) error
+}
+
+type followRelationService struct {
+ repo repository.FollowRepository
+}
+
+func (f *followRelationService) CancelFollow(ctx context.Context, follower, followee int64) error {
+ return f.repo.InactiveFollowRelation(ctx, follower, followee)
+}
+
+func NewFollowRelationService(repo repository.FollowRepository) FollowRelationService {
+ return &followRelationService{
+ repo: repo,
+ }
+}
+
+func (f *followRelationService) GetFollowee(ctx context.Context,
+ follower, offset, limit int64) ([]domain.FollowRelation, error) {
+ return f.repo.GetFollowee(ctx, follower, offset, limit)
+}
+
+func (f *followRelationService) FollowInfo(ctx context.Context, follower, followee int64) (domain.FollowRelation, error) {
+ val, err := f.repo.FollowInfo(ctx, follower, followee)
+ return val, err
+}
+
+func (f *followRelationService) Follow(ctx context.Context, follower, followee int64) error {
+ return f.repo.AddFollowRelation(ctx, domain.FollowRelation{
+ Followee: followee,
+ Follower: follower,
+ })
+}
diff --git a/webook/follow/wire.go b/webook/follow/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..99ede1029b6a1a675689a23a97a124880000a0ae
--- /dev/null
+++ b/webook/follow/wire.go
@@ -0,0 +1,34 @@
+//go:build wireinject
+
+package main
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/follow/grpc"
+ "gitee.com/geekbang/basic-go/webook/follow/ioc"
+ "gitee.com/geekbang/basic-go/webook/follow/repository"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/follow/service"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewGORMFollowRelationDAO,
+ repository.NewFollowRelationRepository,
+ service.NewFollowRelationService,
+ grpc2.NewFollowRelationServiceServer,
+)
+
+var thirdProvider = wire.NewSet(
+ ioc.InitDB,
+ ioc.InitLogger,
+)
+
+func Init() *App {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ ioc.InitGRPCxServer,
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/follow/wire_gen.go b/webook/follow/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..b634604c3619c441bab8f766a2661607323a12fc
--- /dev/null
+++ b/webook/follow/wire_gen.go
@@ -0,0 +1,38 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/follow/grpc"
+ "gitee.com/geekbang/basic-go/webook/follow/ioc"
+ "gitee.com/geekbang/basic-go/webook/follow/repository"
+ "gitee.com/geekbang/basic-go/webook/follow/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/follow/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func Init() *App {
+ loggerV1 := ioc.InitLogger()
+ db := ioc.InitDB(loggerV1)
+ followRelationDao := dao.NewGORMFollowRelationDAO(db)
+ followRelationRepository := repository.NewFollowRelationRepository(followRelationDao)
+ followRelationService := service.NewFollowRelationService(followRelationRepository)
+ followRelationServiceServer := grpc.NewFollowRelationServiceServer(followRelationService)
+ server := ioc.InitGRPCxServer(followRelationServiceServer)
+ app := &App{
+ server: server,
+ }
+ return app
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewGORMFollowRelationDAO, repository.NewFollowRelationRepository, service.NewFollowRelationService, grpc.NewFollowRelationServiceServer)
+
+var thirdProvider = wire.NewSet(ioc.InitDB, ioc.InitLogger)
diff --git a/webook/im/domain/user.go b/webook/im/domain/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ed4c30755bb073657a807e4e931861efd6220ff
--- /dev/null
+++ b/webook/im/domain/user.go
@@ -0,0 +1,8 @@
+package domain
+
+type User struct {
+ ID int64
+ Nickname string
+ // 头像
+ Avatar string
+}
diff --git a/webook/im/events/mysql_binlog_event.go b/webook/im/events/mysql_binlog_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7f116497c1dd4eedfa726538d7ef4ddf84cef22
--- /dev/null
+++ b/webook/im/events/mysql_binlog_event.go
@@ -0,0 +1,85 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/im/domain"
+ "gitee.com/geekbang/basic-go/webook/im/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/canalx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+type MySQLBinlogConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+ svc service.UserService
+}
+
+func (r *MySQLBinlogConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("im_users_sync",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"webook_binlog"},
+ // 监听 User 的数据
+ // 不能直接用过 DAO,因为 DAO 是别的模块的
+ saramax.NewHandler[canalx.Message[User]](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (r *MySQLBinlogConsumer) Consume(msg *sarama.ConsumerMessage,
+ cmsg canalx.Message[User]) error {
+ // 别的表的 binlog,你不关心
+ // 可以考虑,不同的表用不同的 topic,那么你这里就不需要判定了
+ if cmsg.Table != "users" {
+ return nil
+ }
+
+ // 删除用户
+ if cmsg.Type == "DELETE" {
+ // 你不管都可以
+ return nil
+ }
+ // 要在这里更新缓存了
+ // 增删改的消息,实际上在 publish article 里面是没有删的消息的
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ for _, data := range cmsg.Data {
+ // 在这里处理了
+ // 在这里,把用户数据同步过去
+ // 这边能不能把 SyncUser 改成批量接口?可以的,只是说当下是没有必要的。
+ // 万一后面有批量插入用户,或者批量更新,你就把这里改成批量接口
+ err := r.svc.SyncUser(ctx, domain.User{
+ ID: data.Id,
+ Nickname: data.Nickname,
+ // 已有代码没有头像字段
+ //Avatar: data.
+ })
+ if err != nil {
+ // 记录日志下一条
+ continue
+ }
+ }
+ return nil
+}
+
+type User struct {
+ Id int64 `json:"id"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ Nickname string `json:"nickname"`
+ Phone string `json:"phone"`
+ WechatUnionID string `json:"wechat_union_id"`
+ WechatOpenID string `json:"wechat_open_id"`
+ Ctime int64 `json:"ctime"`
+ Utime int64 `json:"utime"`
+}
diff --git a/webook/im/events/user.go b/webook/im/events/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..957f92bcedcd109bebe94b9abaaebd566bb5f028
--- /dev/null
+++ b/webook/im/events/user.go
@@ -0,0 +1,3 @@
+package events
+
+// 在这里监听 canal 的事件,完成数据同步
diff --git a/webook/im/service/user.go b/webook/im/service/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..2644237f672b10605a5367e80726e319f9278d2f
--- /dev/null
+++ b/webook/im/service/user.go
@@ -0,0 +1,90 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/im/domain"
+ "github.com/ecodeclub/ekit/net/httpx"
+ "github.com/google/uuid"
+ "go.opentelemetry.io/otel/trace"
+ "net/http"
+ "strconv"
+)
+
+// 你可以用 localhost
+const defaultEndpoint = "http://localhost:10002/user/user_register"
+
+type UserService interface {
+ SyncUser(ctx context.Context, user domain.User) error
+}
+
+type RESTUserService struct {
+ endpoint string
+ secret string
+ client *http.Client
+}
+
+// secret 从哪里来?
+// 默认就是 openIM123
+func NewUserService(secret string) *RESTUserService {
+ // 假如说我有 TLS 之类的认证,我在这里可以灵活替换具体的 client
+ return &RESTUserService{endpoint: defaultEndpoint,
+ client: http.DefaultClient}
+}
+
+func (s *RESTUserService) SyncUser(ctx context.Context, user domain.User) error {
+ spanCtx := trace.SpanContextFromContext(ctx)
+ // 如果你本身有链路追踪
+ var operationID string
+ // 有这个
+ if spanCtx.HasTraceID() {
+ operationID = spanCtx.TraceID().String()
+ } else {
+ // 实际上也就是你这边没有接入 otel,或者断开了。你的链路追踪断开了
+ operationID = uuid.New().String()
+ }
+ // 现在也就是我要发请求调用 openim 的接口了
+ // httpx 是我封装过的
+ // 这个东西本身就是批量接口
+ var resp response
+
+ err := httpx.NewRequest(ctx, http.MethodPost, s.endpoint).
+ JSONBody(Request{Secret: s.secret, Users: []User{
+ {
+ UserID: strconv.FormatInt(user.ID, 10),
+ Nickname: user.Nickname,
+ FaceURL: user.Avatar,
+ }}}).
+ Client(s.client).
+ AddHeader("operationID", operationID).
+ // 发起请求,拿到 response
+ Do().
+ // 我把请求体转为一个结构体
+ JSONScan(&resp)
+ if err != nil {
+ return err
+ }
+ if resp.ErrCode != 0 {
+ // 也是出错了
+ return fmt.Errorf("同步用户数据失败 %v", resp)
+ }
+ return nil
+}
+
+type response struct {
+ ErrCode int `json:"errCode"`
+ ErrMsg string `json:"errMsg"`
+ ErrDlt string `json:"errDlt"`
+}
+
+type Request struct {
+ Secret string `json:"secret"`
+ Users []User `json:"users"`
+}
+
+type User struct {
+ UserID string `json:"userID"`
+ Nickname string `json:"nickname"`
+ // 头像
+ FaceURL string `json:"faceURL"`
+}
diff --git a/webook/interactive/app.go b/webook/interactive/app.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbeb2d07c06db9dfe06de92a4372b0b1a39c6798
--- /dev/null
+++ b/webook/interactive/app.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+)
+
+type App struct {
+ // 在这里,所有需要 main 函数控制启动、关闭的,都会在这里有一个
+ // 核心就是为了控制生命周期
+ server *grpcx.Server
+ consumers []saramax.Consumer
+ webAdmin *ginx.Server
+}
diff --git a/webook/interactive/client_test.go b/webook/interactive/client_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7123c96de9bd372aad7e3be241d195d8acde5a0
--- /dev/null
+++ b/webook/interactive/client_test.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+ "context"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "testing"
+)
+
+func TestGRPCClient(t *testing.T) {
+ cc, err := grpc.Dial("localhost:8090",
+ grpc.WithTransportCredentials(insecure.NewCredentials()))
+ require.NoError(t, err)
+ client := intrv1.NewInteractiveServiceClient(cc)
+ resp, err := client.Get(context.Background(), &intrv1.GetRequest{
+ Biz: "test",
+ BizId: 2,
+ Uid: 345,
+ })
+ require.NoError(t, err)
+ t.Log(resp.Intr)
+}
+
+func TestGRPCDoubleWrite(t *testing.T) {
+ // 写个 for 循环来模拟
+ cc, err := grpc.Dial("localhost:8090",
+ grpc.WithTransportCredentials(insecure.NewCredentials()))
+ require.NoError(t, err)
+ client := intrv1.NewInteractiveServiceClient(cc)
+ _, err = client.IncrReadCnt(context.Background(), &intrv1.IncrReadCntRequest{
+ Biz: "test",
+ BizId: 2,
+ })
+ require.NoError(t, err)
+}
diff --git a/webook/interactive/config/dev.yaml b/webook/interactive/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8d621a1e9fc3bf70cbec148f4b71f97f65a54c67
--- /dev/null
+++ b/webook/interactive/config/dev.yaml
@@ -0,0 +1,27 @@
+db:
+ src:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+ dst:
+ dsn: "root:root@tcp(localhost:13316)/webook_intr"
+
+migrator:
+ pattern: "SRC_ONLY"
+ web:
+ addr: ":8082"
+
+redis:
+ addr: "localhost:6379"
+
+kafka:
+ addrs:
+ - "localhost:9094"
+
+grpc:
+ server:
+ port: 8090
+ etcdAddrs:
+ - "localhost:12379"
+# client:
+# user:
+# addr: "user.mycompany.com:8090"
+# intr:
\ No newline at end of file
diff --git a/webook/interactive/domain/interactive.go b/webook/interactive/domain/interactive.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf3ba1a53db152f7e3c8141491af19d5acaedc31
--- /dev/null
+++ b/webook/interactive/domain/interactive.go
@@ -0,0 +1,15 @@
+package domain
+
+// Interactive 这个是总体交互的计数
+type Interactive struct {
+ Biz string
+ BizId int64
+
+ ReadCnt int64 `json:"read_cnt"`
+ LikeCnt int64 `json:"like_cnt"`
+ CollectCnt int64 `json:"collect_cnt"`
+ // 这个是当下这个资源,你有没有点赞或者收集
+ // 你也可以考虑把这两个字段分离出去,作为一个单独的结构体
+ Liked bool `json:"liked"`
+ Collected bool `json:"collected"`
+}
diff --git a/webook/interactive/events/article_read_event.go b/webook/interactive/events/article_read_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..31064e4e4729d3421132b4706ae12ee3472828a1
--- /dev/null
+++ b/webook/interactive/events/article_read_event.go
@@ -0,0 +1,6 @@
+package events
+
+type ReadEvent struct {
+ Uid int64
+ Aid int64
+}
diff --git a/webook/interactive/events/batch_consumer.go b/webook/interactive/events/batch_consumer.go
new file mode 100644
index 0000000000000000000000000000000000000000..5de0a3dcd090f0487bace9d42962c43ebbcfdd76
--- /dev/null
+++ b/webook/interactive/events/batch_consumer.go
@@ -0,0 +1,56 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+type InteractiveReadEventBatchConsumer struct {
+ client sarama.Client
+ repo repository.InteractiveRepository
+ l logger.LoggerV1
+}
+
+func NewInteractiveReadEventBatchConsumer(client sarama.Client, repo repository.InteractiveRepository, l logger.LoggerV1) *InteractiveReadEventBatchConsumer {
+ return &InteractiveReadEventBatchConsumer{client: client, repo: repo, l: l}
+}
+
+func (r *InteractiveReadEventBatchConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("interactive",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"read_article"},
+ saramax.NewBatchHandler[ReadEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+// Consume 这个不是幂等的
+func (r *InteractiveReadEventBatchConsumer) Consume(msg []*sarama.ConsumerMessage, ts []ReadEvent) error {
+ ids := make([]int64, 0, len(ts))
+ bizs := make([]string, 0, len(ts))
+ for _, evt := range ts {
+ ids = append(ids, evt.Aid)
+ bizs = append(bizs, "article")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ err := r.repo.BatchIncrReadCnt(ctx, bizs, ids)
+ if err != nil {
+ r.l.Error("批量增加阅读计数失败",
+ logger.Field{Key: "ids", Value: ids},
+ logger.Error(err))
+ }
+ return nil
+}
diff --git a/webook/interactive/events/consumer.go b/webook/interactive/events/consumer.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd229491f852d705be8fa854b17982700e4af600
--- /dev/null
+++ b/webook/interactive/events/consumer.go
@@ -0,0 +1,94 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/canalx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/validator"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "gorm.io/gorm"
+ "sync/atomic"
+ "time"
+)
+
+type MySQLBinlogConsumer[T migrator.Entity] struct {
+ client sarama.Client
+ l logger.LoggerV1
+ table string
+ srcToDst *validator.CanalIncrValidator[T]
+ dstToSrc *validator.CanalIncrValidator[T]
+ dstFirst *atomic.Bool
+}
+
+func NewMySQLBinlogConsumer[T migrator.Entity](
+ client sarama.Client,
+ l logger.LoggerV1,
+ table string,
+ src *gorm.DB,
+ dst *gorm.DB,
+ p events.Producer) *MySQLBinlogConsumer[T] {
+ srcToDst := validator.NewCanalIncrValidator[T](src, dst, "SRC", l, p)
+ dstToSrc := validator.NewCanalIncrValidator[T](src, dst, "DST", l, p)
+ return &MySQLBinlogConsumer[T]{
+ client: client, l: l,
+ dstFirst: &atomic.Bool{},
+ srcToDst: srcToDst,
+ dstToSrc: dstToSrc,
+ table: table}
+}
+
+func (r *MySQLBinlogConsumer[T]) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("migrator_incr",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"webook_binlog"},
+ saramax.NewHandler[canalx.Message[T]](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (r *MySQLBinlogConsumer[T]) Consume(msg *sarama.ConsumerMessage,
+ val canalx.Message[T]) error {
+ // 是不是源表为准
+ dstFirst := r.dstFirst.Load()
+ var v *validator.CanalIncrValidator[T]
+ // db:
+ // src:
+ // dsn: "root:root@tcp(localhost:13316)/webook"
+ // dst:
+ // dsn: "root:root@tcp(localhost:13316)/webook_intr"
+ if dstFirst && val.Database == "webook_intr" {
+ // 目标表为准
+ // 校验,用 dst 的来校验
+ v = r.dstToSrc
+ } else if !dstFirst && val.Database == "webook" {
+ // 源表为准,并且消息恰好来自源表
+ // 校验,用 src 来校验
+ v = r.srcToDst
+ }
+ if v != nil {
+ for _, data := range val.Data {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err := v.Validate(ctx, data.ID())
+ cancel()
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (r *MySQLBinlogConsumer[T]) DstFirst() {
+ r.dstFirst.Store(true)
+}
diff --git a/webook/interactive/grpc/doc.go b/webook/interactive/grpc/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..dc941ba68dfc630faf2584aaa8f518284eefdd4d
--- /dev/null
+++ b/webook/interactive/grpc/doc.go
@@ -0,0 +1,2 @@
+// Package grpc 是用来将业务暴露成为一个 GRPC 接口的
+package grpc
diff --git a/webook/interactive/grpc/server.go b/webook/interactive/grpc/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..4506afdf4f83c7759b3f1434fba7161d8d47db27
--- /dev/null
+++ b/webook/interactive/grpc/server.go
@@ -0,0 +1,88 @@
+package grpc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ domain2 "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+)
+
+// InteractiveServiceServer 我这里只是把 service 包装成一个 grpc 而已
+// 和 grpc 有关的操作,就限定在这里
+type InteractiveServiceServer struct {
+ intrv1.UnimplementedInteractiveServiceServer
+ // 注意,核心业务逻辑一定是在 service 里面的
+ svc service.InteractiveService
+}
+
+func NewInteractiveServiceServer(svc service.InteractiveService) *InteractiveServiceServer {
+ return &InteractiveServiceServer{svc: svc}
+}
+
+func (i *InteractiveServiceServer) Register(server *grpc.Server) {
+ intrv1.RegisterInteractiveServiceServer(server, i)
+}
+
+func (i *InteractiveServiceServer) IncrReadCnt(ctx context.Context, request *intrv1.IncrReadCntRequest) (*intrv1.IncrReadCntResponse, error) {
+ err := i.svc.IncrReadCnt(ctx, request.GetBiz(), request.GetBizId())
+ return &intrv1.IncrReadCntResponse{}, err
+}
+
+func (i *InteractiveServiceServer) Like(ctx context.Context, request *intrv1.LikeRequest) (*intrv1.LikeResponse, error) {
+ err := i.svc.Like(ctx, request.GetBiz(), request.GetBizId(), request.GetUid())
+ return &intrv1.LikeResponse{}, err
+}
+
+func (i *InteractiveServiceServer) CancelLike(ctx context.Context, request *intrv1.CancelLikeRequest) (*intrv1.CancelLikeResponse, error) {
+ // 也可以考虑用 grpc 的插件
+ if request.Uid <= 0 {
+ return nil, status.Error(codes.InvalidArgument, "uid 错误")
+ }
+ err := i.svc.CancelLike(ctx, request.GetBiz(), request.GetBizId(), request.GetUid())
+ return &intrv1.CancelLikeResponse{}, err
+}
+
+func (i *InteractiveServiceServer) Collect(ctx context.Context, request *intrv1.CollectRequest) (*intrv1.CollectResponse, error) {
+ err := i.svc.Collect(ctx, request.GetBiz(), request.GetBizId(), request.GetUid(), request.GetCid())
+ return &intrv1.CollectResponse{}, err
+}
+
+func (i *InteractiveServiceServer) Get(ctx context.Context, request *intrv1.GetRequest) (*intrv1.GetResponse, error) {
+ res, err := i.svc.Get(ctx, request.GetBiz(), request.GetBizId(), request.GetUid())
+ if err != nil {
+ return nil, err
+ }
+ return &intrv1.GetResponse{
+ Intr: i.toDTO(res),
+ }, nil
+}
+
+func (i *InteractiveServiceServer) GetByIds(ctx context.Context, request *intrv1.GetByIdsRequest) (*intrv1.GetByIdsResponse, error) {
+ res, err := i.svc.GetByIds(ctx, request.GetBiz(), request.GetIds())
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[int64]*intrv1.Interactive, len(res))
+ for k, v := range res {
+ m[k] = i.toDTO(v)
+ }
+ return &intrv1.GetByIdsResponse{
+ Intrs: m,
+ }, nil
+}
+
+// DTO data transfer object
+func (i *InteractiveServiceServer) toDTO(intr domain2.Interactive) *intrv1.Interactive {
+ return &intrv1.Interactive{
+ Biz: intr.Biz,
+ BizId: intr.BizId,
+ CollectCnt: intr.CollectCnt,
+ Collected: intr.Collected,
+ LikeCnt: intr.LikeCnt,
+ Liked: intr.Liked,
+ ReadCnt: intr.ReadCnt,
+ }
+}
diff --git a/webook/interactive/integration/gen_data_test.go b/webook/interactive/integration/gen_data_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f0bc8cf47541640a1b82fe89c444565d14a2d00f
--- /dev/null
+++ b/webook/interactive/integration/gen_data_test.go
@@ -0,0 +1,101 @@
+package integration
+
+import (
+ _ "embed"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/internal/integration/startup"
+ "github.com/stretchr/testify/require"
+ "gorm.io/gorm"
+ "math/rand"
+ "os"
+ "strconv"
+ "testing"
+ "time"
+)
+
+//go:embed init.sql
+var initSQL string
+
+// TestGenSQL 这个测试就是用来生成 SQL,你在启动 Docker 的时候导入进去。
+func TestGenSQL(t *testing.T) {
+ file, err := os.OpenFile("data.sql",
+ os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC, 0666)
+ require.NoError(t, err)
+ defer file.Close()
+
+ // 创建数据库和数据表的语句,防止还没初始化
+ _, err = file.WriteString(initSQL)
+ require.NoError(t, err)
+
+ const prefix = "INSERT INTO `interactives`(`biz_id`, `biz`, `read_cnt`, `collect_cnt`, `like_cnt`, `ctime`, `utime`)\nVALUES"
+ const rowNum = 1000
+
+ now := time.Now().UnixMilli()
+ _, err = file.WriteString(prefix)
+
+ for i := 0; i < rowNum; i++ {
+ if i > 0 {
+ file.Write([]byte{',', '\n'})
+ }
+ file.Write([]byte{'('})
+ // biz_id
+ file.WriteString(strconv.Itoa(i + 1))
+ // biz
+ file.WriteString(`,"test",`)
+ // read_cnt
+ file.WriteString(strconv.Itoa(int(rand.Int31n(10000))))
+ file.Write([]byte{','})
+
+ // collect_cnt
+ file.WriteString(strconv.Itoa(int(rand.Int31n(10000))))
+ file.Write([]byte{','})
+ // like_cnt
+ file.WriteString(strconv.Itoa(int(rand.Int31n(10000))))
+ file.Write([]byte{','})
+
+ // ctime
+ file.WriteString(strconv.FormatInt(now, 10))
+ file.Write([]byte{','})
+
+ // utime
+ file.WriteString(strconv.FormatInt(now, 10))
+
+ file.Write([]byte{')'})
+ }
+}
+
+func TestGenData(t *testing.T) {
+ // 这个是批量插入,数据量不是特别大的时候,可以用这个
+ // GenData 要比 GenSQL 慢
+ // 你根据自己的需要调整批次,和每个批次大小
+ db := startup.InitTestDB()
+ // 这个为 true,只会输出 SQL,但是不会执行,也不会报错
+ // db.DryRun = true
+ // 1000 批
+ for i := 0; i < 10; i++ {
+ // 每次 100 条
+ // 你可以考虑直接用 CreateInBatches,GORM 帮你分批次
+ // 我自己分是为了控制内存消耗
+ const batchSize = 100
+ data := make([]dao.Interactive, 0, batchSize)
+ now := time.Now().UnixMilli()
+ for j := 0; j < batchSize; j++ {
+ data = append(data, dao.Interactive{
+ Biz: "test",
+ BizId: int64(i*batchSize + j + 1),
+ ReadCnt: rand.Int63(),
+ LikeCnt: rand.Int63(),
+ CollectCnt: rand.Int63(),
+ Utime: now,
+ Ctime: now,
+ })
+ }
+
+ err := db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Create(data).Error
+ require.NoError(t, err)
+ return err
+ })
+ require.NoError(t, err)
+ }
+}
diff --git a/webook/interactive/integration/init.sql b/webook/interactive/integration/init.sql
new file mode 100644
index 0000000000000000000000000000000000000000..d8636dc0b79fe62f427bc01897e3763ae3324457
--- /dev/null
+++ b/webook/interactive/integration/init.sql
@@ -0,0 +1,47 @@
+create database if not exists webook;
+create table if not exists webook.interactives
+(
+ id bigint auto_increment
+ primary key,
+ biz_id bigint null,
+ biz varchar(128) null,
+ read_cnt bigint null,
+ collect_cnt bigint null,
+ like_cnt bigint null,
+ ctime bigint null,
+ utime bigint null,
+ constraint biz_type_id
+ unique (biz_id, biz)
+);
+
+create table if not exists webook.user_collection_bizs
+(
+ id bigint auto_increment
+ primary key,
+ cid bigint null,
+ biz_id bigint null,
+ biz varchar(128) null,
+ uid bigint null,
+ ctime bigint null,
+ utime bigint null,
+ constraint biz_type_id_uid
+ unique (biz_id, biz, uid)
+);
+
+create index idx_user_collection_bizs_cid
+ on webook.user_collection_bizs (cid);
+
+create table if not exists webook.user_like_bizs
+(
+ id bigint auto_increment
+ primary key,
+ biz_id bigint null,
+ biz varchar(128) null,
+ uid bigint null,
+ status tinyint unsigned null,
+ ctime bigint null,
+ utime bigint null,
+ constraint biz_type_id_uid
+ unique (biz_id, biz, uid)
+);
+
diff --git a/webook/interactive/integration/interactive_svc_test.go b/webook/interactive/integration/interactive_svc_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a9402395fb10975f5815cba2d348baf0e3de5403
--- /dev/null
+++ b/webook/interactive/integration/interactive_svc_test.go
@@ -0,0 +1,809 @@
+package integration
+
+import (
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ "gitee.com/geekbang/basic-go/webook/interactive/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+ "golang.org/x/net/context"
+ "gorm.io/gorm"
+ "testing"
+ "time"
+)
+
+type InteractiveTestSuite struct {
+ suite.Suite
+ db *gorm.DB
+ rdb redis.Cmdable
+ server *grpc.InteractiveServiceServer
+}
+
+func (s *InteractiveTestSuite) SetupSuite() {
+ s.db = startup.InitTestDB()
+ s.rdb = startup.InitRedis()
+ s.server = startup.InitInteractiveGRPCServer()
+}
+
+func (s *InteractiveTestSuite) TearDownTest() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.Exec("TRUNCATE TABLE `interactives`").Error
+ assert.NoError(s.T(), err)
+ err = s.db.Exec("TRUNCATE TABLE `user_like_bizs`").Error
+ assert.NoError(s.T(), err)
+ err = s.db.Exec("TRUNCATE TABLE `user_collection_bizs`").Error
+ assert.NoError(s.T(), err)
+ // 清空 Redis
+ err = s.rdb.FlushDB(ctx).Err()
+ assert.NoError(s.T(), err)
+}
+
+func (s *InteractiveTestSuite) TestIncrReadCnt() {
+ testCases := []struct {
+ name string
+ before func(t *testing.T)
+ after func(t *testing.T)
+
+ biz string
+ bizId int64
+
+ wantErr error
+ wantResp *intrv1.IncrReadCntResponse
+ }{
+ {
+ // DB 和缓存都有数据
+ name: "增加成功,db和redis",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.Create(dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ Utime: 7,
+ }).Error
+ assert.NoError(t, err)
+ err = s.rdb.HSet(ctx, "interactive:test:2",
+ "read_cnt", 3).Err()
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("id = ?", 1).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 7)
+ data.Utime = 0
+ assert.Equal(t, dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ // +1 之后
+ ReadCnt: 4,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ }, data)
+ cnt, err := s.rdb.HGet(ctx, "interactive:test:2", "read_cnt").Int()
+ assert.NoError(t, err)
+ assert.Equal(t, 4, cnt)
+ err = s.rdb.Del(ctx, "interactive:test:2").Err()
+ assert.NoError(t, err)
+ },
+ biz: "test",
+ bizId: 2,
+ wantResp: &intrv1.IncrReadCntResponse{},
+ },
+ {
+ // DB 有数据,缓存没有数据
+ name: "增加成功,db有",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.WithContext(ctx).Create(dao.Interactive{
+ Id: 3,
+ Biz: "test",
+ BizId: 3,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ Utime: 7,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("id = ?", 3).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 7)
+ data.Utime = 0
+ assert.Equal(t, dao.Interactive{
+ Id: 3,
+ Biz: "test",
+ BizId: 3,
+ // +1 之后
+ ReadCnt: 4,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ }, data)
+ cnt, err := s.rdb.Exists(ctx, "interactive:test:3").Result()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), cnt)
+ },
+ biz: "test",
+ bizId: 3,
+ wantResp: &intrv1.IncrReadCntResponse{},
+ },
+ {
+ name: "增加成功-都没有",
+ before: func(t *testing.T) {},
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("biz = ? AND biz_id = ?", "test", 4).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 0)
+ assert.True(t, data.Ctime > 0)
+ assert.True(t, data.Id > 0)
+ data.Id = 0
+ data.Utime = 0
+ data.Ctime = 0
+ assert.Equal(t, dao.Interactive{
+ Biz: "test",
+ BizId: 4,
+ ReadCnt: 1,
+ }, data)
+ cnt, err := s.rdb.Exists(ctx, "interactive:test:4").Result()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), cnt)
+ },
+ biz: "test",
+ bizId: 4,
+ wantResp: &intrv1.IncrReadCntResponse{},
+ },
+ }
+
+ // 不同于 AsyncSms 服务,我们不需要 mock,所以创建一个就可以
+ // 不需要每个测试都创建
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ resp, err := s.server.IncrReadCnt(context.Background(), &intrv1.IncrReadCntRequest{
+ Biz: tc.biz, BizId: tc.bizId,
+ })
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantResp, resp)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *InteractiveTestSuite) TestLike() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ before func(t *testing.T)
+ after func(t *testing.T)
+
+ biz string
+ bizId int64
+ uid int64
+
+ wantErr error
+ wantResp *intrv1.LikeResponse
+ }{
+ {
+ name: "点赞-DB和cache都有",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.Create(dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ Utime: 7,
+ }).Error
+ assert.NoError(t, err)
+ err = s.rdb.HSet(ctx, "interactive:test:2",
+ "like_cnt", 3).Err()
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("id = ?", 1).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 7)
+ data.Utime = 0
+ assert.Equal(t, dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 6,
+ Ctime: 6,
+ }, data)
+
+ var likeBiz dao.UserLikeBiz
+ err = s.db.Where("biz = ? AND biz_id = ? AND uid = ?",
+ "test", 2, 123).First(&likeBiz).Error
+ assert.NoError(t, err)
+ assert.True(t, likeBiz.Id > 0)
+ assert.True(t, likeBiz.Ctime > 0)
+ assert.True(t, likeBiz.Utime > 0)
+ likeBiz.Id = 0
+ likeBiz.Ctime = 0
+ likeBiz.Utime = 0
+ assert.Equal(t, dao.UserLikeBiz{
+ Biz: "test",
+ BizId: 2,
+ Uid: 123,
+ Status: 1,
+ }, likeBiz)
+
+ cnt, err := s.rdb.HGet(ctx, "interactive:test:2", "like_cnt").Int()
+ assert.NoError(t, err)
+ assert.Equal(t, 4, cnt)
+ err = s.rdb.Del(ctx, "interactive:test:2").Err()
+ assert.NoError(t, err)
+ },
+ biz: "test",
+ bizId: 2,
+ uid: 123,
+ wantResp: &intrv1.LikeResponse{},
+ },
+ {
+ name: "点赞-都没有",
+ before: func(t *testing.T) {},
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("biz = ? AND biz_id = ?", "test", 3).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 0)
+ assert.True(t, data.Ctime > 0)
+ assert.True(t, data.Id > 0)
+ data.Utime = 0
+ data.Ctime = 0
+ data.Id = 0
+ assert.Equal(t, dao.Interactive{
+ Biz: "test",
+ BizId: 3,
+ LikeCnt: 1,
+ }, data)
+
+ var likeBiz dao.UserLikeBiz
+ err = s.db.Where("biz = ? AND biz_id = ? AND uid = ?",
+ "test", 3, 123).First(&likeBiz).Error
+ assert.NoError(t, err)
+ assert.True(t, likeBiz.Id > 0)
+ assert.True(t, likeBiz.Ctime > 0)
+ assert.True(t, likeBiz.Utime > 0)
+ likeBiz.Id = 0
+ likeBiz.Ctime = 0
+ likeBiz.Utime = 0
+ assert.Equal(t, dao.UserLikeBiz{
+ Biz: "test",
+ BizId: 3,
+ Uid: 123,
+ Status: 1,
+ }, likeBiz)
+
+ cnt, err := s.rdb.Exists(ctx, "interactive:test:2").Result()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), cnt)
+ },
+ biz: "test",
+ bizId: 3,
+ uid: 123,
+ wantResp: &intrv1.LikeResponse{},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ resp, err := s.server.Like(context.Background(), &intrv1.LikeRequest{
+ Biz: tc.biz, BizId: tc.bizId, Uid: tc.uid,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResp, resp)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *InteractiveTestSuite) TestDislike() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ before func(t *testing.T)
+ after func(t *testing.T)
+
+ biz string
+ bizId int64
+ uid int64
+
+ wantErr error
+ wantResp *intrv1.CancelLikeResponse
+ }{
+ {
+ name: "取消点赞-DB和cache都有",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.Create(dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 5,
+ Ctime: 6,
+ Utime: 7,
+ }).Error
+ assert.NoError(t, err)
+ err = s.db.Create(dao.UserLikeBiz{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ Uid: 123,
+ Ctime: 6,
+ Utime: 7,
+ Status: 1,
+ }).Error
+ assert.NoError(t, err)
+ err = s.rdb.HSet(ctx, "interactive:test:2",
+ "like_cnt", 3).Err()
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ var data dao.Interactive
+ err := s.db.Where("id = ?", 1).First(&data).Error
+ assert.NoError(t, err)
+ assert.True(t, data.Utime > 7)
+ data.Utime = 0
+ assert.Equal(t, dao.Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 3,
+ CollectCnt: 4,
+ LikeCnt: 4,
+ Ctime: 6,
+ }, data)
+
+ var likeBiz dao.UserLikeBiz
+ err = s.db.Where("id = ?", 1).First(&likeBiz).Error
+ assert.NoError(t, err)
+ assert.True(t, likeBiz.Utime > 7)
+ likeBiz.Utime = 0
+ assert.Equal(t, dao.UserLikeBiz{
+ Id: 1,
+ Biz: "test",
+ BizId: 2,
+ Uid: 123,
+ Ctime: 6,
+ Status: 0,
+ }, likeBiz)
+
+ cnt, err := s.rdb.HGet(ctx, "interactive:test:2", "like_cnt").Int()
+ assert.NoError(t, err)
+ assert.Equal(t, 2, cnt)
+ err = s.rdb.Del(ctx, "interactive:test:2").Err()
+ assert.NoError(t, err)
+ },
+ biz: "test",
+ bizId: 2,
+ uid: 123,
+ wantResp: &intrv1.CancelLikeResponse{},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ resp, err := s.server.CancelLike(context.Background(), &intrv1.CancelLikeRequest{
+ Biz: tc.biz, BizId: tc.bizId, Uid: tc.uid,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResp, resp)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *InteractiveTestSuite) TestCollect() {
+ testCases := []struct {
+ name string
+
+ before func(t *testing.T)
+ after func(t *testing.T)
+
+ bizId int64
+ biz string
+ cid int64
+ uid int64
+
+ wantErr error
+ wantResp *intrv1.CollectResponse
+ }{
+ {
+ name: "收藏成功,db和缓存都没有",
+ before: func(t *testing.T) {},
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ var intr dao.Interactive
+ err := s.db.Where("biz = ? AND biz_id = ?", "test", 1).First(&intr).Error
+ assert.NoError(t, err)
+ assert.True(t, intr.Ctime > 0)
+ intr.Ctime = 0
+ assert.True(t, intr.Utime > 0)
+ intr.Utime = 0
+ assert.True(t, intr.Id > 0)
+ intr.Id = 0
+ assert.Equal(t, dao.Interactive{
+ Biz: "test",
+ BizId: 1,
+ CollectCnt: 1,
+ }, intr)
+ cnt, err := s.rdb.Exists(ctx, "interactive:test:1").Result()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), cnt)
+ // 收藏记录
+ var cbiz dao.UserCollectionBiz
+ err = s.db.WithContext(ctx).
+ Where("uid = ? AND biz = ? AND biz_id = ?", 1, "test", 1).
+ First(&cbiz).Error
+ assert.NoError(t, err)
+ assert.True(t, cbiz.Ctime > 0)
+ cbiz.Ctime = 0
+ assert.True(t, cbiz.Utime > 0)
+ cbiz.Utime = 0
+ assert.True(t, cbiz.Id > 0)
+ cbiz.Id = 0
+ assert.Equal(t, dao.UserCollectionBiz{
+ Biz: "test",
+ BizId: 1,
+ Cid: 1,
+ Uid: 1,
+ }, cbiz)
+ },
+ bizId: 1,
+ biz: "test",
+ cid: 1,
+ uid: 1,
+ wantResp: &intrv1.CollectResponse{},
+ },
+ {
+ name: "收藏成功,db有缓存没有",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ err := s.db.WithContext(ctx).Create(&dao.Interactive{
+ Biz: "test",
+ BizId: 2,
+ CollectCnt: 10,
+ Ctime: 123,
+ Utime: 234,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ var intr dao.Interactive
+ err := s.db.WithContext(ctx).
+ Where("biz = ? AND biz_id = ?", "test", 2).First(&intr).Error
+ assert.NoError(t, err)
+ assert.True(t, intr.Ctime > 0)
+ intr.Ctime = 0
+ assert.True(t, intr.Utime > 0)
+ intr.Utime = 0
+ assert.True(t, intr.Id > 0)
+ intr.Id = 0
+ assert.Equal(t, dao.Interactive{
+ Biz: "test",
+ BizId: 2,
+ CollectCnt: 11,
+ }, intr)
+ cnt, err := s.rdb.Exists(ctx, "interactive:test:2").Result()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), cnt)
+
+ var cbiz dao.UserCollectionBiz
+ err = s.db.WithContext(ctx).
+ Where("uid = ? AND biz = ? AND biz_id = ?", 1, "test", 2).
+ First(&cbiz).Error
+ assert.NoError(t, err)
+ assert.True(t, cbiz.Ctime > 0)
+ cbiz.Ctime = 0
+ assert.True(t, cbiz.Utime > 0)
+ cbiz.Utime = 0
+ assert.True(t, cbiz.Id > 0)
+ cbiz.Id = 0
+ assert.Equal(t, dao.UserCollectionBiz{
+ Biz: "test",
+ BizId: 2,
+ Cid: 1,
+ Uid: 1,
+ }, cbiz)
+ },
+ bizId: 2,
+ biz: "test",
+ cid: 1,
+ uid: 1,
+ wantResp: &intrv1.CollectResponse{},
+ },
+ {
+ name: "收藏成功,db和缓存都有",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ err := s.db.WithContext(ctx).Create(&dao.Interactive{
+ Biz: "test",
+ BizId: 3,
+ CollectCnt: 10,
+ Ctime: 123,
+ Utime: 234,
+ }).Error
+ assert.NoError(t, err)
+ err = s.rdb.HSet(ctx, "interactive:test:3", "collect_cnt", 10).Err()
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ var intr dao.Interactive
+ err := s.db.WithContext(ctx).
+ Where("biz = ? AND biz_id = ?", "test", 3).First(&intr).Error
+ assert.NoError(t, err)
+ assert.True(t, intr.Ctime > 0)
+ intr.Ctime = 0
+ assert.True(t, intr.Utime > 0)
+ intr.Utime = 0
+ assert.True(t, intr.Id > 0)
+ intr.Id = 0
+ assert.Equal(t, dao.Interactive{
+ Biz: "test",
+ BizId: 3,
+ CollectCnt: 11,
+ }, intr)
+ cnt, err := s.rdb.HGet(ctx, "interactive:test:3", "collect_cnt").Int()
+ assert.NoError(t, err)
+ assert.Equal(t, 11, cnt)
+
+ var cbiz dao.UserCollectionBiz
+ err = s.db.WithContext(ctx).
+ Where("uid = ? AND biz = ? AND biz_id = ?", 1, "test", 3).
+ First(&cbiz).Error
+ assert.NoError(t, err)
+ assert.True(t, cbiz.Ctime > 0)
+ cbiz.Ctime = 0
+ assert.True(t, cbiz.Utime > 0)
+ cbiz.Utime = 0
+ assert.True(t, cbiz.Id > 0)
+ cbiz.Id = 0
+ assert.Equal(t, dao.UserCollectionBiz{
+ Biz: "test",
+ BizId: 3,
+ Cid: 1,
+ Uid: 1,
+ }, cbiz)
+ },
+ bizId: 3,
+ biz: "test",
+ cid: 1,
+ uid: 1,
+ wantResp: &intrv1.CollectResponse{},
+ },
+ }
+
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ resp, err := s.server.Collect(context.Background(), &intrv1.CollectRequest{
+ Biz: tc.biz, BizId: tc.bizId, Cid: tc.cid, Uid: tc.uid,
+ })
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantResp, resp)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *InteractiveTestSuite) TestGet() {
+ testCases := []struct {
+ name string
+
+ before func(t *testing.T)
+
+ bizId int64
+ biz string
+ uid int64
+
+ wantErr error
+ wantRes *intrv1.GetResponse
+ }{
+ {
+ name: "全部取出来了-无缓存",
+ biz: "test",
+ bizId: 12,
+ uid: 123,
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.WithContext(ctx).Create(&dao.Interactive{
+ Biz: "test",
+ BizId: 12,
+ ReadCnt: 100,
+ CollectCnt: 200,
+ LikeCnt: 300,
+ Ctime: 123,
+ Utime: 234,
+ }).Error
+ assert.NoError(t, err)
+ },
+ wantRes: &intrv1.GetResponse{
+ Intr: &intrv1.Interactive{
+ Biz: "test",
+ BizId: 12,
+ ReadCnt: 100,
+ CollectCnt: 200,
+ LikeCnt: 300,
+ },
+ },
+ },
+ {
+ name: "全部取出来了-命中缓存-用户已点赞收藏",
+ biz: "test",
+ bizId: 3,
+ uid: 123,
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.db.WithContext(ctx).
+ Create(&dao.UserCollectionBiz{
+ Cid: 1,
+ Biz: "test",
+ BizId: 3,
+ Uid: 123,
+ Ctime: 123,
+ Utime: 124,
+ }).Error
+ assert.NoError(t, err)
+ err = s.db.WithContext(ctx).
+ Create(&dao.UserLikeBiz{
+ Biz: "test",
+ BizId: 3,
+ Uid: 123,
+ Ctime: 123,
+ Utime: 124,
+ Status: 1,
+ }).Error
+ assert.NoError(t, err)
+ err = s.rdb.HSet(ctx, "interactive:test:3",
+ "read_cnt", 0, "collect_cnt", 1).Err()
+ assert.NoError(t, err)
+ },
+ wantRes: &intrv1.GetResponse{
+ Intr: &intrv1.Interactive{
+ BizId: 3,
+ CollectCnt: 1,
+ Collected: true,
+ Liked: true,
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ res, err := s.server.Get(context.Background(), &intrv1.GetRequest{
+ Biz: tc.biz, BizId: tc.bizId, Uid: tc.uid,
+ })
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantRes, res)
+ })
+ }
+}
+
+func (s *InteractiveTestSuite) TestGetByIds() {
+ preCtx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+
+ // 准备数据
+ for i := 1; i < 5; i++ {
+ i := int64(i)
+ err := s.db.WithContext(preCtx).
+ Create(&dao.Interactive{
+ Id: i,
+ Biz: "test",
+ BizId: i,
+ ReadCnt: i,
+ CollectCnt: i + 1,
+ LikeCnt: i + 2,
+ }).Error
+ assert.NoError(s.T(), err)
+ }
+
+ testCases := []struct {
+ name string
+
+ before func(t *testing.T)
+ biz string
+ ids []int64
+
+ wantErr error
+ wantRes *intrv1.GetByIdsResponse
+ }{
+ {
+ name: "查找成功",
+ biz: "test",
+ ids: []int64{1, 2},
+ wantRes: &intrv1.GetByIdsResponse{
+ Intrs: map[int64]*intrv1.Interactive{
+ 1: {
+ Biz: "test",
+ BizId: 1,
+ ReadCnt: 1,
+ CollectCnt: 2,
+ LikeCnt: 3,
+ },
+ 2: {
+ Biz: "test",
+ BizId: 2,
+ ReadCnt: 2,
+ CollectCnt: 3,
+ LikeCnt: 4,
+ },
+ },
+ },
+ },
+ {
+ name: "没有对应的数据",
+ biz: "test",
+ ids: []int64{100, 200},
+ wantRes: &intrv1.GetByIdsResponse{
+ Intrs: map[int64]*intrv1.Interactive{},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ res, err := s.server.GetByIds(context.Background(), &intrv1.GetByIdsRequest{
+ Biz: tc.biz, Ids: tc.ids,
+ })
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantRes, res)
+ })
+ }
+}
+
+func TestInteractiveService(t *testing.T) {
+ suite.Run(t, &InteractiveTestSuite{})
+}
diff --git a/webook/interactive/integration/startup/db.go b/webook/interactive/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..25911e215ca98b1d64d639d3159f36171f9e0352
--- /dev/null
+++ b/webook/interactive/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTable(db)
+ if err != nil {
+ panic(err)
+ }
+ db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/interactive/integration/startup/log.go b/webook/interactive/integration/startup/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..a659ad9dbf326536df6bc5e6641a4aed105b15bc
--- /dev/null
+++ b/webook/interactive/integration/startup/log.go
@@ -0,0 +1,9 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+func InitLog() logger.LoggerV1 {
+ return logger.NewNoOpLogger()
+}
diff --git a/webook/interactive/integration/startup/redis.go b/webook/interactive/integration/startup/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4d929d011e0aa42e75aa0c6c68fb582c39c8db7
--- /dev/null
+++ b/webook/interactive/integration/startup/redis.go
@@ -0,0 +1,21 @@
+package startup
+
+import (
+ "context"
+ "github.com/redis/go-redis/v9"
+)
+
+var redisClient redis.Cmdable
+
+func InitRedis() redis.Cmdable {
+ if redisClient == nil {
+ redisClient = redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ })
+
+ for err := redisClient.Ping(context.Background()).Err(); err != nil; {
+ panic(err)
+ }
+ }
+ return redisClient
+}
diff --git a/webook/interactive/integration/startup/wire.go b/webook/interactive/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..51130789dfaf4824038260f2db45229aa770c60d
--- /dev/null
+++ b/webook/interactive/integration/startup/wire.go
@@ -0,0 +1,31 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ repository2 "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ cache2 "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ service2 "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "github.com/google/wire"
+)
+
+var thirdProvider = wire.NewSet(InitRedis,
+ InitTestDB, InitLog)
+var interactiveSvcProvider = wire.NewSet(
+ service2.NewInteractiveService,
+ repository2.NewCachedInteractiveRepository,
+ dao2.NewGORMInteractiveDAO,
+ cache2.NewRedisInteractiveCache,
+)
+
+func InitInteractiveService() service2.InteractiveService {
+ wire.Build(thirdProvider, interactiveSvcProvider)
+ return service2.NewInteractiveService(nil, nil)
+}
+
+func InitInteractiveGRPCServer() *grpc.InteractiveServiceServer {
+ wire.Build(thirdProvider, interactiveSvcProvider, grpc.NewInteractiveServiceServer)
+ return new(grpc.InteractiveServiceServer)
+}
diff --git a/webook/interactive/integration/startup/wire_gen.go b/webook/interactive/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..031d105391e40eb7baeb21509ee206c346cb2987
--- /dev/null
+++ b/webook/interactive/integration/startup/wire_gen.go
@@ -0,0 +1,48 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitInteractiveService() service.InteractiveService {
+ gormDB := InitTestDB()
+ interactiveDAO := dao.NewGORMInteractiveDAO(gormDB)
+ cmdable := InitRedis()
+ interactiveCache := cache.NewRedisInteractiveCache(cmdable)
+ loggerV1 := InitLog()
+ interactiveRepository := repository.NewCachedInteractiveRepository(interactiveDAO, interactiveCache, loggerV1)
+ interactiveService := service.NewInteractiveService(interactiveRepository, loggerV1)
+ return interactiveService
+}
+
+func InitInteractiveGRPCServer() *grpc.InteractiveServiceServer {
+ gormDB := InitTestDB()
+ interactiveDAO := dao.NewGORMInteractiveDAO(gormDB)
+ cmdable := InitRedis()
+ interactiveCache := cache.NewRedisInteractiveCache(cmdable)
+ loggerV1 := InitLog()
+ interactiveRepository := repository.NewCachedInteractiveRepository(interactiveDAO, interactiveCache, loggerV1)
+ interactiveService := service.NewInteractiveService(interactiveRepository, loggerV1)
+ interactiveServiceServer := grpc.NewInteractiveServiceServer(interactiveService)
+ return interactiveServiceServer
+}
+
+// wire.go:
+
+var thirdProvider = wire.NewSet(InitRedis,
+ InitTestDB, InitLog)
+
+var interactiveSvcProvider = wire.NewSet(service.NewInteractiveService, repository.NewCachedInteractiveRepository, dao.NewGORMInteractiveDAO, cache.NewRedisInteractiveCache)
diff --git a/webook/interactive/ioc/db.go b/webook/interactive/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..7323353b89d2928f286a7db2fb82a5c1bf2b3f70
--- /dev/null
+++ b/webook/interactive/ioc/db.go
@@ -0,0 +1,205 @@
+package ioc
+
+import (
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/gormx/connpool"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ promsdk "github.com/prometheus/client_golang/prometheus"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "time"
+)
+
+func InitSRC(l logger.LoggerV1) SrcDB {
+ return InitDB(l, "src")
+}
+
+func InitDST(l logger.LoggerV1) DstDB {
+ return InitDB(l, "dst")
+}
+
+func InitDoubleWritePool(src SrcDB, dst DstDB) *connpool.DoubleWritePool {
+ pattern := viper.GetString("migrator.pattern")
+ return connpool.NewDoubleWritePool(src.ConnPool, dst.ConnPool, pattern)
+}
+
+// 这个是业务用的,支持双写的 DB
+func InitBizDB(pool *connpool.DoubleWritePool) *gorm.DB {
+ db, err := gorm.Open(mysql.New(mysql.Config{
+ Conn: pool,
+ }))
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type SrcDB *gorm.DB
+type DstDB *gorm.DB
+
+func InitDB(l logger.LoggerV1, key string) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ var cfg = Config{
+ DSN: "root:root@tcp(localhost:13316)/webook_default",
+ }
+ err := viper.UnmarshalKey("db."+key, &cfg)
+ db, err := gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{})
+ if err != nil {
+ panic(err)
+ }
+
+ cb := newCallbacks(key)
+ err = db.Use(cb)
+ if err != nil {
+ panic(err)
+ }
+
+ // 这里已经删掉了 prometheus,
+ err = dao2.InitTable(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type Callbacks struct {
+ vector *promsdk.SummaryVec
+}
+
+func (pcb *Callbacks) Name() string {
+ return "prometheus-query"
+}
+
+func (pcb *Callbacks) Initialize(db *gorm.DB) error {
+ pcb.registerAll(db)
+ return nil
+}
+
+func newCallbacks(key string) *Callbacks {
+ vector := promsdk.NewSummaryVec(promsdk.SummaryOpts{
+ // 在这边,你要考虑设置各种 Namespace
+ Namespace: "geekbang_daming",
+ Subsystem: "webook_" + key,
+ Name: "gorm_query_time",
+ Help: "统计 GORM 的执行时间",
+ ConstLabels: map[string]string{
+ "db": "webook",
+ },
+ Objectives: map[float64]float64{
+ 0.5: 0.01,
+ 0.9: 0.01,
+ 0.99: 0.005,
+ 0.999: 0.0001,
+ },
+ },
+ // 如果是 JOIN 查询,table 就是 JOIN 在一起的
+ // 或者 table 就是主表,A JOIN B,记录的是 A
+ []string{"type", "table"})
+
+ pcb := &Callbacks{
+ vector: vector,
+ }
+ promsdk.MustRegister(vector)
+ return pcb
+}
+
+func (pcb *Callbacks) registerAll(db *gorm.DB) {
+ // 作用于 INSERT 语句
+ err := db.Callback().Create().Before("*").
+ Register("prometheus_create_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Create().After("*").
+ Register("prometheus_create_after", pcb.after("create"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Update().Before("*").
+ Register("prometheus_update_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Update().After("*").
+ Register("prometheus_update_after", pcb.after("update"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Delete().Before("*").
+ Register("prometheus_delete_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Delete().After("*").
+ Register("prometheus_delete_after", pcb.after("delete"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Raw().Before("*").
+ Register("prometheus_raw_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Raw().After("*").
+ Register("prometheus_raw_after", pcb.after("raw"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Row().Before("*").
+ Register("prometheus_row_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Row().After("*").
+ Register("prometheus_row_after", pcb.after("row"))
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (c *Callbacks) before() func(db *gorm.DB) {
+ return func(db *gorm.DB) {
+ startTime := time.Now()
+ db.Set("start_time", startTime)
+ }
+}
+
+func (c *Callbacks) after(typ string) func(db *gorm.DB) {
+ return func(db *gorm.DB) {
+ val, _ := db.Get("start_time")
+ startTime, ok := val.(time.Time)
+ if !ok {
+ // 你啥都干不了
+ return
+ }
+ table := db.Statement.Table
+ if table == "" {
+ table = "unknown"
+ }
+ c.vector.WithLabelValues(typ, table).
+ Observe(float64(time.Since(startTime).Milliseconds()))
+ }
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
+
+type DoSomething interface {
+ DoABC() string
+}
+
+type DoSomethingFunc func() string
+
+func (d DoSomethingFunc) DoABC() string {
+ return d()
+}
diff --git a/webook/interactive/ioc/grpc.go b/webook/interactive/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc6fb54e52e6241074812c67a0f682faffe5e8b7
--- /dev/null
+++ b/webook/interactive/ioc/grpc.go
@@ -0,0 +1,37 @@
+package ioc
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(
+ l logger.LoggerV1,
+ intrServer *grpc2.InteractiveServiceServer) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddrs []string `yaml:"etcdAddrs"`
+ }
+
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ // master 分支
+ //err := viper.UnmarshalKey("grpc", &cfg)
+ if err != nil {
+ panic(err)
+ }
+
+ server := grpc.NewServer()
+ intrServer.Register(server)
+
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ EtcdAddrs: cfg.EtcdAddrs,
+ Name: "interactive",
+ L: l,
+ }
+}
diff --git a/webook/interactive/ioc/kafka.go b/webook/interactive/ioc/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd4ce218546d8ccde5c28df1920c436c54d5c14f
--- /dev/null
+++ b/webook/interactive/ioc/kafka.go
@@ -0,0 +1,49 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/events"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events/fixer"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "github.com/spf13/viper"
+)
+
+func InitKafka() sarama.Client {
+ type Config struct {
+ Addrs []string `yaml:"addrs"`
+ }
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ var cfg Config
+ err := viper.UnmarshalKey("kafka", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := sarama.NewClient(cfg.Addrs, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+func InitSyncProducer(client sarama.Client) sarama.SyncProducer {
+ res, err := sarama.NewSyncProducerFromClient(client)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
+
+// 规避 wire 的问题
+type fixerInteractive *fixer.Consumer[dao.Interactive]
+
+// NewConsumers 面临的问题依旧是所有的 Consumer 在这里注册一下
+func NewConsumers(intr *events.InteractiveReadEventConsumer,
+ fix *fixer.Consumer[dao.Interactive],
+) []saramax.Consumer {
+ return []saramax.Consumer{
+ intr,
+ fix,
+ }
+}
diff --git a/webook/interactive/ioc/log.go b/webook/interactive/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2f96c0463bffaf4f05a8de79fc885006a88c232
--- /dev/null
+++ b/webook/interactive/ioc/log.go
@@ -0,0 +1,14 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ l, err := zap.NewDevelopment()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/interactive/ioc/migrator.go b/webook/interactive/ioc/migrator.go
new file mode 100644
index 0000000000000000000000000000000000000000..028d5dd9169aeccca73f374d6d4cb9ff18dda9a6
--- /dev/null
+++ b/webook/interactive/ioc/migrator.go
@@ -0,0 +1,58 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/gormx/connpool"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events/fixer"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/scheduler"
+ "github.com/IBM/sarama"
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/spf13/viper"
+)
+
+const topic = "migrator_interactives"
+
+func InitFixDataConsumer(l logger.LoggerV1,
+ src SrcDB,
+ dst DstDB,
+ client sarama.Client) *fixer.Consumer[dao.Interactive] {
+ res, err := fixer.NewConsumer[dao.Interactive](client, l,
+ topic, src, dst)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
+
+func InitMigradatorProducer(p sarama.SyncProducer) events.Producer {
+ return events.NewSaramaProducer(p, topic)
+}
+
+func InitMigratorWeb(
+ l logger.LoggerV1,
+ src SrcDB,
+ dst DstDB,
+ pool *connpool.DoubleWritePool,
+ producer events.Producer,
+) *ginx.Server {
+ // 在这里,有多少张表,你就初始化多少个 scheduler
+ intrSch := scheduler.NewScheduler[dao.Interactive](l, src, dst, pool, producer)
+ engine := gin.Default()
+ ginx.InitCounter(prometheus.CounterOpts{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook_intr_admin",
+ Name: "http_biz_code",
+ Help: "HTTP 的业务错误码",
+ })
+ intrSch.RegisterRoutes(engine.Group("/migrator"))
+ //intrSch.RegisterRoutes(engine.Group("/migrator/interactive"))
+ addr := viper.GetString("migrator.web.addr")
+ return &ginx.Server{
+ Addr: addr,
+ Engine: engine,
+ }
+}
diff --git a/webook/interactive/ioc/redis.go b/webook/interactive/ioc/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..bae1ec51b75c48e08ca02c54c5f88d0ef437aed2
--- /dev/null
+++ b/webook/interactive/ioc/redis.go
@@ -0,0 +1,14 @@
+package ioc
+
+import (
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+)
+
+func InitRedis() redis.Cmdable {
+ addr := viper.GetString("redis.addr")
+ redisClient := redis.NewClient(&redis.Options{
+ Addr: addr,
+ })
+ return redisClient
+}
diff --git a/webook/interactive/main.go b/webook/interactive/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad10c6edd0091101e5b5f8bdee98876f1d2f1bd9
--- /dev/null
+++ b/webook/interactive/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "fmt"
+ "github.com/fsnotify/fsnotify"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+ "log"
+)
+
+func main() {
+ initViperV1()
+ // 这里暂时随便搞一下
+ // 搞成依赖注入
+ app := InitAPP()
+ for _, c := range app.consumers {
+ err := c.Start()
+ if err != nil {
+ panic(err)
+ }
+ }
+ go func() {
+ err := app.webAdmin.Start()
+ log.Println(err)
+ }()
+ err := app.server.Serve()
+ log.Println(err)
+}
+
+//func mainV1() {
+// initViperV1()
+// server := grpc2.NewServer()
+// // 这里暂时随便搞一下
+// // 搞成依赖注入
+// // 这种写法的缺陷是,如果我有很多个 grpc API 服务端的实现
+// intrSvc := InitGRPCServer()
+// intrv1.RegisterInteractiveServiceServer(server, intrSvc)
+// // 监听 8090 端口,你可以随便写
+// l, err := net.Listen("tcp", ":8090")
+// if err != nil {
+// panic(err)
+// }
+// // 这边会阻塞,类似与 gin.Run
+// err = server.Serve(l)
+// log.Println(err)
+//}
+
+func initViperV1() {
+ cfile := pflag.String("config",
+ "config/config.yaml", "指定配置文件路径")
+ pflag.Parse()
+ viper.SetConfigFile(*cfile)
+ // 实时监听配置变更
+ viper.WatchConfig()
+ // 只能告诉你文件变了,不能告诉你,文件的哪些内容变了
+ viper.OnConfigChange(func(in fsnotify.Event) {
+ // 比较好的设计,它会在 in 里面告诉你变更前的数据,和变更后的数据
+ // 更好的设计是,它会直接告诉你差异。
+ fmt.Println(in.Name, in.Op)
+ fmt.Println(viper.GetString("db.dsn"))
+ })
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/webook/interactive/repository/cache/errors.go b/webook/interactive/repository/cache/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..931b452a228634f18813b0db0128f86e7b3a6bcc
--- /dev/null
+++ b/webook/interactive/repository/cache/errors.go
@@ -0,0 +1,5 @@
+package cache
+
+import "github.com/redis/go-redis/v9"
+
+var ErrKeyNotExist = redis.Nil
diff --git a/webook/interactive/repository/cache/interactive.go b/webook/interactive/repository/cache/interactive.go
new file mode 100644
index 0000000000000000000000000000000000000000..1521867b3fbc427e6f89b915f732a0126dd1b936
--- /dev/null
+++ b/webook/interactive/repository/cache/interactive.go
@@ -0,0 +1,120 @@
+package cache
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "github.com/redis/go-redis/v9"
+ "strconv"
+ "time"
+)
+
+var (
+ //go:embed lua/interative_incr_cnt.lua
+ luaIncrCnt string
+)
+
+const (
+ fieldReadCnt = "read_cnt"
+ fieldCollectCnt = "collect_cnt"
+ fieldLikeCnt = "like_cnt"
+)
+
+//go:generate mockgen -source=./interactive.go -package=cachemocks -destination=mocks/interactive.mock.go InteractiveCache
+type InteractiveCache interface {
+
+ // IncrReadCntIfPresent 如果在缓存中有对应的数据,就 +1
+ IncrReadCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error
+ IncrLikeCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error
+ DecrLikeCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error
+ IncrCollectCntIfPresent(ctx context.Context, biz string, bizId int64) error
+ // Get 查询缓存中数据
+ Get(ctx context.Context, biz string, bizId int64) (domain.Interactive, error)
+ Set(ctx context.Context, biz string, bizId int64, intr domain.Interactive) error
+}
+
+type RedisInteractiveCache struct {
+ client redis.Cmdable
+ expiration time.Duration
+}
+
+func (r *RedisInteractiveCache) IncrCollectCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error {
+ return r.client.Eval(ctx, luaIncrCnt,
+ []string{r.key(biz, bizId)},
+ fieldCollectCnt, 1).Err()
+}
+
+func (r *RedisInteractiveCache) IncrReadCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error {
+ return r.client.Eval(ctx, luaIncrCnt,
+ []string{r.key(biz, bizId)},
+ fieldReadCnt, 1).Err()
+}
+
+func (r *RedisInteractiveCache) IncrLikeCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error {
+ return r.client.Eval(ctx, luaIncrCnt,
+ []string{r.key(biz, bizId)},
+ fieldLikeCnt, 1).Err()
+}
+
+func (r *RedisInteractiveCache) DecrLikeCntIfPresent(ctx context.Context,
+ biz string, bizId int64) error {
+ return r.client.Eval(ctx, luaIncrCnt,
+ []string{r.key(biz, bizId)},
+ fieldLikeCnt, -1).Err()
+}
+
+func (r *RedisInteractiveCache) Get(ctx context.Context,
+ biz string, bizId int64) (domain.Interactive, error) {
+ // 直接使用 HMGet,即便缓存中没有对应的 key,也不会返回 error
+ data, err := r.client.HGetAll(ctx, r.key(biz, bizId)).Result()
+ if err != nil {
+ return domain.Interactive{}, err
+ }
+
+ if len(data) == 0 {
+ // 缓存不存在
+ return domain.Interactive{}, ErrKeyNotExist
+ }
+
+ // 理论上来说,这里不可能有 error
+ collectCnt, _ := strconv.ParseInt(data[fieldCollectCnt], 10, 64)
+ likeCnt, _ := strconv.ParseInt(data[fieldLikeCnt], 10, 64)
+ readCnt, _ := strconv.ParseInt(data[fieldReadCnt], 10, 64)
+
+ return domain.Interactive{
+ // 懒惰的写法
+ BizId: bizId,
+ CollectCnt: collectCnt,
+ LikeCnt: likeCnt,
+ ReadCnt: readCnt,
+ }, err
+}
+
+func (r *RedisInteractiveCache) Set(ctx context.Context, biz string, bizId int64, intr domain.Interactive) error {
+ key := r.key(biz, bizId)
+ err := r.client.HMSet(ctx, key,
+ fieldLikeCnt, intr.LikeCnt,
+ fieldCollectCnt, intr.CollectCnt,
+ fieldReadCnt, intr.ReadCnt).Err()
+ if err != nil {
+ return err
+ }
+ return r.client.Expire(ctx, key, time.Minute*15).Err()
+}
+
+func (r *RedisInteractiveCache) key(biz string, bizId int64) string {
+ return fmt.Sprintf("interactive:%s:%d", biz, bizId)
+}
+
+func NewRedisInteractiveCache(client redis.Cmdable) InteractiveCache {
+ return &RedisInteractiveCache{
+ client: client,
+ }
+}
diff --git a/webook/interactive/repository/cache/lua/interative_incr_cnt.lua b/webook/interactive/repository/cache/lua/interative_incr_cnt.lua
new file mode 100644
index 0000000000000000000000000000000000000000..72183491b0390f4f6e357434418da61823d64535
--- /dev/null
+++ b/webook/interactive/repository/cache/lua/interative_incr_cnt.lua
@@ -0,0 +1,14 @@
+local key = KEYS[1]
+-- 对应到的是 hincrby 中的 field
+local cntKey = ARGV[1]
+-- +1 或者 -1
+local delta = tonumber(ARGV[2])
+local exists = redis.call("EXISTS", key)
+if exists == 1 then
+ redis.call("HINCRBY", key, cntKey, delta)
+ -- 说明自增成功了
+ return 1
+else
+ -- 自增不成功
+ return 0
+end
\ No newline at end of file
diff --git a/webook/interactive/repository/dao/double_write.go b/webook/interactive/repository/dao/double_write.go
new file mode 100644
index 0000000000000000000000000000000000000000..0be269cf5fac43d0c32ec3c33c5d14576ba570ec
--- /dev/null
+++ b/webook/interactive/repository/dao/double_write.go
@@ -0,0 +1,115 @@
+package dao
+
+import (
+ "context"
+ "errors"
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "gorm.io/gorm"
+)
+
+const (
+ patternDstOnly = "DST_ONLY"
+ patternSrcOnly = "SRC_ONLY"
+ patternDstFirst = "DST_FIRST"
+ patternSrcFirst = "SRC_FIRST"
+)
+
+type DoubleWriteDAO struct {
+ src InteractiveDAO
+ dst InteractiveDAO
+ pattern *atomicx.Value[string]
+}
+
+func NewDoubleWriteDAOV1(src *gorm.DB, dst *gorm.DB) *DoubleWriteDAO {
+ return &DoubleWriteDAO{src: NewGORMInteractiveDAO(src),
+ pattern: atomicx.NewValueOf(patternSrcOnly),
+ dst: NewGORMInteractiveDAO(dst)}
+}
+
+func NewDoubleWriteDAO(src InteractiveDAO, dst InteractiveDAO) *DoubleWriteDAO {
+ return &DoubleWriteDAO{src: src,
+ pattern: atomicx.NewValueOf(patternSrcOnly),
+ dst: dst}
+}
+
+// AST + 模板变成=代码生成
+func (d *DoubleWriteDAO) IncrReadCnt(ctx context.Context, biz string, bizId int64) error {
+ switch d.pattern.Load() {
+ case patternSrcOnly:
+ return d.src.IncrReadCnt(ctx, biz, bizId)
+ case patternSrcFirst:
+ err := d.src.IncrReadCnt(ctx, biz, bizId)
+ if err != nil {
+ // 怎么办?
+ // 要不要继续写 DST?
+ // 这里有一个问题,万一,我的 err 是超时错误呢?
+ return err
+ }
+ // 这里有一个问题, SRC 成功了,但是 DST 失败了怎么办?
+ // 等校验与修复
+ err = d.dst.IncrReadCnt(ctx, biz, bizId)
+ if err != nil {
+ // 记日志
+ // dst 写失败,不被认为是失败
+ }
+ return nil
+ case patternDstOnly:
+ return d.dst.IncrReadCnt(ctx, biz, bizId)
+ case patternDstFirst:
+ err := d.dst.IncrReadCnt(ctx, biz, bizId)
+ if err != nil {
+ return err
+ }
+ err = d.src.IncrReadCnt(ctx, biz, bizId)
+ if err != nil {
+ // 记日志
+ // src 写失败,不被认为是失败
+ }
+ return nil
+ default:
+ return errors.New("未知的双写模式")
+ }
+}
+func (d *DoubleWriteDAO) UpdatePattern(pattern string) {
+ d.pattern.Store(pattern)
+}
+
+func (d *DoubleWriteDAO) InsertLikeInfo(ctx context.Context, biz string, bizId, uid int64) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) GetLikeInfo(ctx context.Context, biz string, bizId, uid int64) (UserLikeBiz, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) DeleteLikeInfo(ctx context.Context, biz string, bizId, uid int64) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) Get(ctx context.Context, biz string, bizId int64) (Interactive, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) InsertCollectionBiz(ctx context.Context, cb UserCollectionBiz) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) GetCollectionInfo(ctx context.Context, biz string, bizId, uid int64) (UserCollectionBiz, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) BatchIncrReadCnt(ctx context.Context, bizs []string, ids []int64) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleWriteDAO) GetByIds(ctx context.Context, biz string, ids []int64) ([]Interactive, error) {
+ //TODO implement me
+ panic("implement me")
+}
diff --git a/webook/interactive/repository/dao/init.go b/webook/interactive/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..15a64b73f03e1bb5b8cf06adecfe7c749659b7c0
--- /dev/null
+++ b/webook/interactive/repository/dao/init.go
@@ -0,0 +1,14 @@
+package dao
+
+import (
+ "gorm.io/gorm"
+)
+
+func InitTable(db *gorm.DB) error {
+ return db.AutoMigrate(
+ &Interactive{},
+ &UserLikeBiz{},
+ &Collection{},
+ &UserCollectionBiz{},
+ )
+}
diff --git a/webook/interactive/repository/dao/interactive.go b/webook/interactive/repository/dao/interactive.go
new file mode 100644
index 0000000000000000000000000000000000000000..07f8ac2ebafa990fd658fad1e9533658be7e1a9d
--- /dev/null
+++ b/webook/interactive/repository/dao/interactive.go
@@ -0,0 +1,284 @@
+package dao
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "time"
+)
+
+var ErrDataNotFound = gorm.ErrRecordNotFound
+
+//go:generate mockgen -source=./interactive.go -package=daomocks -destination=mocks/interactive.mock.go InteractiveDAO
+type InteractiveDAO interface {
+ IncrReadCnt(ctx context.Context, biz string, bizId int64) error
+ InsertLikeInfo(ctx context.Context, biz string, bizId, uid int64) error
+ GetLikeInfo(ctx context.Context, biz string, bizId, uid int64) (UserLikeBiz, error)
+ DeleteLikeInfo(ctx context.Context, biz string, bizId, uid int64) error
+ Get(ctx context.Context, biz string, bizId int64) (Interactive, error)
+ InsertCollectionBiz(ctx context.Context, cb UserCollectionBiz) error
+ GetCollectionInfo(ctx context.Context, biz string, bizId, uid int64) (UserCollectionBiz, error)
+ BatchIncrReadCnt(ctx context.Context, bizs []string, ids []int64) error
+ GetByIds(ctx context.Context, biz string, ids []int64) ([]Interactive, error)
+}
+
+type GORMInteractiveDAO struct {
+ db *gorm.DB
+}
+
+func (dao *GORMInteractiveDAO) GetByIds(ctx context.Context, biz string, ids []int64) ([]Interactive, error) {
+ var res []Interactive
+ err := dao.db.WithContext(ctx).Where("biz = ? AND id IN ?", biz, ids).Find(&res).Error
+ return res, err
+}
+
+func (dao *GORMInteractiveDAO) GetLikeInfo(ctx context.Context, biz string, bizId, uid int64) (UserLikeBiz, error) {
+ var res UserLikeBiz
+ err := dao.db.WithContext(ctx).
+ Where("biz=? AND biz_id = ? AND uid = ? AND status = ?",
+ biz, bizId, uid, 1).First(&res).Error
+ return res, err
+}
+
+func (dao *GORMInteractiveDAO) GetCollectionInfo(ctx context.Context, biz string, bizId, uid int64) (UserCollectionBiz, error) {
+ var res UserCollectionBiz
+ err := dao.db.WithContext(ctx).
+ Where("biz=? AND biz_id = ? AND uid = ?", biz, bizId, uid).First(&res).Error
+ return res, err
+}
+
+// InsertCollectionBiz 插入收藏记录,并且更新计数
+func (dao *GORMInteractiveDAO) InsertCollectionBiz(ctx context.Context, cb UserCollectionBiz) error {
+ now := time.Now().UnixMilli()
+ cb.Utime = now
+ cb.Ctime = now
+ return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := dao.db.WithContext(ctx).Create(&cb).Error
+ if err != nil {
+ return err
+ }
+ return tx.Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "collect_cnt": gorm.Expr("`collect_cnt`+1"),
+ "utime": now,
+ }),
+ }).Create(&Interactive{
+ CollectCnt: 1,
+ Ctime: now,
+ Utime: now,
+ Biz: cb.Biz,
+ BizId: cb.BizId,
+ }).Error
+ })
+}
+
+func (dao *GORMInteractiveDAO) InsertLikeInfo(ctx context.Context, biz string, bizId, uid int64) error {
+ now := time.Now().UnixMilli()
+ err := dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := tx.Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "status": 1,
+ "utime": now,
+ }),
+ }).Create(&UserLikeBiz{
+ Uid: uid,
+ Ctime: now,
+ Utime: now,
+ Biz: biz,
+ BizId: bizId,
+ Status: 1,
+ }).Error
+ if err != nil {
+ return err
+ }
+ return tx.Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "like_cnt": gorm.Expr("`like_cnt`+1"),
+ "utime": now,
+ }),
+ }).Create(&Interactive{
+ LikeCnt: 1,
+ Ctime: now,
+ Utime: now,
+ Biz: biz,
+ BizId: bizId,
+ }).Error
+ })
+ return err
+}
+
+func (dao *GORMInteractiveDAO) DeleteLikeInfo(ctx context.Context, biz string, bizId, uid int64) error {
+ now := time.Now().UnixMilli()
+ err := dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(&UserLikeBiz{}).
+ Where("biz =? AND biz_id = ? AND uid = ?", biz, bizId, uid).
+ Updates(map[string]any{
+ "status": 0,
+ "utime": now,
+ }).Error
+ if err != nil {
+ return err
+ }
+ return dao.db.WithContext(ctx).Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "like_cnt": gorm.Expr("`like_cnt`-1"),
+ "utime": now,
+ }),
+ }).Create(&Interactive{
+ LikeCnt: 1,
+ Ctime: now,
+ Utime: now,
+ Biz: biz,
+ BizId: bizId,
+ }).Error
+ })
+ return err
+}
+
+func NewGORMInteractiveDAO(db *gorm.DB) InteractiveDAO {
+ return &GORMInteractiveDAO{
+ db: db,
+ }
+}
+
+// IncrReadCnt 是一个插入或者更新语义
+func (dao *GORMInteractiveDAO) IncrReadCnt(ctx context.Context, biz string, bizId int64) error {
+ return dao.incrReadCnt(dao.db.WithContext(ctx), biz, bizId)
+}
+
+func (dao *GORMInteractiveDAO) incrReadCnt(tx *gorm.DB, biz string, bizId int64) error {
+ now := time.Now().UnixMilli()
+ return tx.Clauses(clause.OnConflict{
+ DoUpdates: clause.Assignments(map[string]any{
+ "read_cnt": gorm.Expr("`read_cnt`+1"),
+ "utime": now,
+ }),
+ }).Create(&Interactive{
+ ReadCnt: 1,
+ Ctime: now,
+ Utime: now,
+ Biz: biz,
+ BizId: bizId,
+ }).Error
+}
+
+func (dao *GORMInteractiveDAO) BatchIncrReadCnt(ctx context.Context, bizs []string, ids []int64) error {
+ return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ // 让调用者保证两者是相等的
+ for i := 0; i < len(bizs); i++ {
+ err := dao.incrReadCnt(tx, bizs[i], ids[i])
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func (dao *GORMInteractiveDAO) Get(ctx context.Context, biz string, bizId int64) (Interactive, error) {
+ var res Interactive
+ err := dao.db.WithContext(ctx).
+ Where("biz = ? AND biz_id = ?", biz, bizId).
+ First(&res).Error
+ return res, err
+}
+
+// 正常来说,一张主表和与它有关联关系的表会共用一个DAO,
+// 所以我们就用一个 DAO 来操作
+
+type Interactive struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ BizId int64 `gorm:"uniqueIndex:biz_type_id"`
+ Biz string `gorm:"type:varchar(128);uniqueIndex:biz_type_id"`
+ ReadCnt int64
+ CollectCnt int64
+ // 作业:就是直接在 LikeCnt 上创建一个索引
+ // 1. 而后查询前 100 的,直接就命中索引,这样你前 100 最多 100 次回表
+ // SELECT * FROM interactives ORDER BY like_cnt limit 0, 100
+ // 还有一种优化思路是
+ // SELECT * FROM interactives WHERE like_cnt > 1000 ORDER BY like_cnt limit 0, 100
+ // 2. 如果你只需要 biz_id 和 biz_type,你就创建联合索引
+ LikeCnt int64
+ Ctime int64
+ Utime int64
+}
+
+func (i Interactive) ID() int64 {
+ return i.Id
+}
+
+func (i Interactive) CompareTo(dst migrator.Entity) bool {
+ dstVal, ok := dst.(Interactive)
+ return ok && i == dstVal
+}
+
+// UserLikeBiz 命名无能,用户点赞的某个东西
+type UserLikeBiz struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ // 三个构成唯一索引
+ BizId int64 `gorm:"uniqueIndex:biz_type_id_uid"`
+ Biz string `gorm:"type:varchar(128);uniqueIndex:biz_type_id_uid"`
+ Uid int64 `gorm:"uniqueIndex:biz_type_id_uid"`
+ // 依旧是只在 DB 层面生效的状态
+ // 1- 有效,0-无效。软删除的用法
+ Status uint8
+ Ctime int64
+ Utime int64
+}
+
+// Collection 收藏夹
+type Collection struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ Name string `gorm:"type=varchar(1024)"`
+ Uid int64 `gorm:""`
+
+ Ctime int64
+ Utime int64
+}
+
+// UserCollectionBiz 收藏的东西
+type UserCollectionBiz struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ // 收藏夹 ID
+ // 作为关联关系中的外键,我们这里需要索引
+ Cid int64 `gorm:"index"`
+ BizId int64 `gorm:"uniqueIndex:biz_type_id_uid"`
+ Biz string `gorm:"type:varchar(128);uniqueIndex:biz_type_id_uid"`
+ // 这算是一个冗余,因为正常来说,
+ // 只需要在 Collection 中维持住 Uid 就可以
+ Uid int64 `gorm:"uniqueIndex:biz_type_id_uid"`
+ Ctime int64
+ Utime int64
+}
+
+func multipleCh() {
+ ch0 := make(chan msg, 100000)
+ ch1 := make(chan msg, 100000)
+ go func() {
+ for {
+ var m msg
+ select {
+ case ch0 <- m:
+ default:
+ ch1 <- m
+ }
+ }
+ }()
+
+ go func() {
+ for {
+ var m msg
+ select {
+ case ch1 <- m:
+ default:
+ ch0 <- m
+ }
+ }
+ }()
+}
+
+type msg struct {
+ biz string
+ bizId int64
+}
diff --git a/webook/interactive/repository/interactive.go b/webook/interactive/repository/interactive.go
new file mode 100644
index 0000000000000000000000000000000000000000..f983bf811a1a74c7f86aa29cec3ccf51afad9338
--- /dev/null
+++ b/webook/interactive/repository/interactive.go
@@ -0,0 +1,156 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/ecodeclub/ekit/slice"
+)
+
+//go:generate mockgen -source=./interactive.go -package=repomocks -destination=mocks/interactive.mock.go InteractiveRepository
+type InteractiveRepository interface {
+ IncrReadCnt(ctx context.Context,
+ biz string, bizId int64) error
+ // BatchIncrReadCnt 这里调用者要保证 bizs 和 bizIds 长度一样
+ BatchIncrReadCnt(ctx context.Context, bizs []string, bizIds []int64) error
+ IncrLike(ctx context.Context, biz string, bizId, uid int64) error
+ DecrLike(ctx context.Context, biz string, bizId, uid int64) error
+ AddCollectionItem(ctx context.Context, biz string, bizId, cid int64, uid int64) error
+ Get(ctx context.Context, biz string, bizId int64) (domain.Interactive, error)
+ Liked(ctx context.Context, biz string, id int64, uid int64) (bool, error)
+ Collected(ctx context.Context, biz string, id int64, uid int64) (bool, error)
+ GetByIds(ctx context.Context, biz string, ids []int64) ([]domain.Interactive, error)
+}
+
+type CachedReadCntRepository struct {
+ cache cache.InteractiveCache
+ dao dao.InteractiveDAO
+ l logger.LoggerV1
+}
+
+func (c *CachedReadCntRepository) GetByIds(ctx context.Context, biz string, ids []int64) ([]domain.Interactive, error) {
+ vals, err := c.dao.GetByIds(ctx, biz, ids)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map[dao.Interactive, domain.Interactive](vals,
+ func(idx int, src dao.Interactive) domain.Interactive {
+ return c.toDomain(src)
+ }), nil
+}
+
+func (c *CachedReadCntRepository) Liked(ctx context.Context, biz string, id int64, uid int64) (bool, error) {
+ _, err := c.dao.GetLikeInfo(ctx, biz, id, uid)
+ switch err {
+ case nil:
+ return true, nil
+ case dao.ErrDataNotFound:
+ return false, nil
+ default:
+ return false, err
+ }
+}
+
+func (c *CachedReadCntRepository) Collected(ctx context.Context, biz string, id int64, uid int64) (bool, error) {
+ _, err := c.dao.GetCollectionInfo(ctx, biz, id, uid)
+ switch err {
+ case nil:
+ return true, nil
+ case dao.ErrDataNotFound:
+ return false, nil
+ default:
+ return false, err
+ }
+}
+
+func (c *CachedReadCntRepository) IncrLike(ctx context.Context,
+ biz string, bizId int64, uid int64) error {
+ err := c.dao.InsertLikeInfo(ctx, biz, bizId, uid)
+ if err != nil {
+ return err
+ }
+ return c.cache.IncrLikeCntIfPresent(ctx, biz, bizId)
+}
+
+func (c *CachedReadCntRepository) DecrLike(ctx context.Context,
+ biz string, bizId int64, uid int64) error {
+ err := c.dao.DeleteLikeInfo(ctx, biz, bizId, uid)
+ if err != nil {
+ return err
+ }
+ return c.cache.DecrLikeCntIfPresent(ctx, biz, bizId)
+}
+
+func (c *CachedReadCntRepository) IncrReadCnt(ctx context.Context,
+ biz string, bizId int64) error {
+ err := c.dao.IncrReadCnt(ctx, biz, bizId)
+ if err != nil {
+ return err
+ }
+ // 这边会有部分失败引起的不一致的问题,但是你其实不需要解决,
+ // 因为阅读数不准确完全没有问题
+ return c.cache.IncrReadCntIfPresent(ctx, biz, bizId)
+}
+
+func (c *CachedReadCntRepository) BatchIncrReadCnt(ctx context.Context,
+ bizs []string, bizIds []int64) error {
+ return c.dao.BatchIncrReadCnt(ctx, bizs, bizIds)
+}
+
+func (c *CachedReadCntRepository) AddCollectionItem(ctx context.Context,
+ biz string, bizId, cid, uid int64) error {
+ err := c.dao.InsertCollectionBiz(ctx, dao.UserCollectionBiz{
+ Biz: biz,
+ Cid: cid,
+ BizId: bizId,
+ Uid: uid,
+ })
+ if err != nil {
+ return err
+ }
+ return c.cache.IncrCollectCntIfPresent(ctx, biz, bizId)
+}
+
+func (c *CachedReadCntRepository) Get(ctx context.Context,
+ biz string, bizId int64) (domain.Interactive, error) {
+ intr, err := c.cache.Get(ctx, biz, bizId)
+ if err == nil {
+ // 缓存只缓存了具体的数字,但是没有缓存自身有没有点赞的信息
+ // 因为一个人反复刷,重复刷一篇文章是小概率的事情
+ // 也就是说,你缓存了某个用户是否点赞的数据,命中率会很低
+ return intr, nil
+ }
+ ie, err := c.dao.Get(ctx, biz, bizId)
+ if err == nil {
+ res := c.toDomain(ie)
+ if er := c.cache.Set(ctx, biz, bizId, res); er != nil {
+ c.l.Error("回写缓存失败",
+ logger.Int64("bizId", bizId),
+ logger.String("biz", biz),
+ logger.Error(er))
+ }
+ return res, nil
+ }
+ return domain.Interactive{}, err
+}
+
+func (c *CachedReadCntRepository) toDomain(intr dao.Interactive) domain.Interactive {
+ return domain.Interactive{
+ Biz: intr.Biz,
+ BizId: intr.BizId,
+ LikeCnt: intr.LikeCnt,
+ CollectCnt: intr.CollectCnt,
+ ReadCnt: intr.ReadCnt,
+ }
+}
+
+func NewCachedInteractiveRepository(dao dao.InteractiveDAO,
+ cache cache.InteractiveCache, l logger.LoggerV1) InteractiveRepository {
+ return &CachedReadCntRepository{
+ dao: dao,
+ cache: cache,
+ l: l,
+ }
+}
diff --git a/webook/interactive/service/interactive.go b/webook/interactive/service/interactive.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ac10fd57fc32cc1f677185fa1d975de906452f7
--- /dev/null
+++ b/webook/interactive/service/interactive.go
@@ -0,0 +1,97 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "golang.org/x/sync/errgroup"
+)
+
+//go:generate mockgen -source=interactive.go -package=svcmocks -destination=mocks/interactive.mock.go InteractiveService
+type InteractiveService interface {
+ IncrReadCnt(ctx context.Context, biz string, bizId int64) error
+ // Like 点赞
+ Like(ctx context.Context, biz string, bizId int64, uid int64) error
+ // CancelLike 取消点赞
+ CancelLike(ctx context.Context, biz string, bizId int64, uid int64) error
+ // Collect 收藏
+ Collect(ctx context.Context, biz string, bizId, cid, uid int64) error
+ Get(ctx context.Context, biz string, bizId, uid int64) (domain.Interactive, error)
+ GetByIds(ctx context.Context, biz string, bizIds []int64) (map[int64]domain.Interactive, error)
+}
+
+type interactiveService struct {
+ repo repository.InteractiveRepository
+ l logger.LoggerV1
+}
+
+func (i *interactiveService) GetByIds(ctx context.Context, biz string,
+ bizIds []int64) (map[int64]domain.Interactive, error) {
+ intrs, err := i.repo.GetByIds(ctx, biz, bizIds)
+ if err != nil {
+ return nil, err
+ }
+ res := make(map[int64]domain.Interactive, len(intrs))
+ for _, intr := range intrs {
+ res[intr.BizId] = intr
+ }
+ return res, nil
+}
+
+func (i *interactiveService) IncrReadCnt(ctx context.Context, biz string, bizId int64) error {
+ return i.repo.IncrReadCnt(ctx, biz, bizId)
+}
+
+func (i *interactiveService) Get(
+ ctx context.Context, biz string,
+ bizId, uid int64) (domain.Interactive, error) {
+ // 你也可以考虑将分发的逻辑也下沉到 repository 里面
+ intr, err := i.repo.Get(ctx, biz, bizId)
+ if err != nil {
+ return domain.Interactive{}, err
+ }
+ var eg errgroup.Group
+ eg.Go(func() error {
+ intr.Liked, err = i.repo.Liked(ctx, biz, bizId, uid)
+ return err
+ })
+ eg.Go(func() error {
+ intr.Collected, err = i.repo.Collected(ctx, biz, bizId, uid)
+ return err
+ })
+ // 说明是登录过的,补充用户是否点赞或者
+ // 新的打印日志的形态 zap 本身就有这种用法
+ err = eg.Wait()
+ if err != nil {
+ // 这个查询失败只需要记录日志就可以,不需要中断执行
+ i.l.Error("查询用户是否点赞的信息失败",
+ logger.String("biz", biz),
+ logger.Int64("bizId", bizId),
+ logger.Int64("uid", uid),
+ logger.Error(err))
+ }
+ return intr, nil
+}
+
+func (i *interactiveService) Like(ctx context.Context, biz string, bizId int64, uid int64) error {
+ return i.repo.IncrLike(ctx, biz, bizId, uid)
+}
+
+func (i *interactiveService) CancelLike(ctx context.Context, biz string, bizId int64, uid int64) error {
+ return i.repo.DecrLike(ctx, biz, bizId, uid)
+}
+
+// Collect 收藏
+func (i *interactiveService) Collect(ctx context.Context,
+ biz string, bizId, cid, uid int64) error {
+ return i.repo.AddCollectionItem(ctx, biz, bizId, cid, uid)
+}
+
+func NewInteractiveService(repo repository.InteractiveRepository,
+ l logger.LoggerV1) InteractiveService {
+ return &interactiveService{
+ repo: repo,
+ l: l,
+ }
+}
diff --git a/webook/interactive/wire.go b/webook/interactive/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..703059bb7fd0fa8cb8bc50b15c47fd3b16dfa0a8
--- /dev/null
+++ b/webook/interactive/wire.go
@@ -0,0 +1,50 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/events"
+ "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ "gitee.com/geekbang/basic-go/webook/interactive/ioc"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "github.com/google/wire"
+)
+
+var thirdPartySet = wire.NewSet(
+ ioc.InitDST,
+ ioc.InitSRC,
+ ioc.InitBizDB,
+ ioc.InitDoubleWritePool,
+ ioc.InitLogger,
+ ioc.InitKafka,
+ // 暂时不理会 consumer 怎么启动
+ ioc.InitSyncProducer,
+ ioc.InitRedis)
+
+var interactiveSvcProvider = wire.NewSet(
+ service.NewInteractiveService,
+ repository.NewCachedInteractiveRepository,
+ dao.NewGORMInteractiveDAO,
+ cache.NewRedisInteractiveCache,
+)
+
+var migratorProvider = wire.NewSet(
+ ioc.InitMigratorWeb,
+ ioc.InitFixDataConsumer,
+ ioc.InitMigradatorProducer)
+
+func InitAPP() *App {
+ wire.Build(interactiveSvcProvider,
+ thirdPartySet,
+ migratorProvider,
+ events.NewInteractiveReadEventConsumer,
+ grpc.NewInteractiveServiceServer,
+ ioc.NewConsumers,
+ ioc.InitGRPCxServer,
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/interactive/wire_gen.go b/webook/interactive/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..f3a900d8de6d7c48ba8cda3818c7ec14e1d3d45c
--- /dev/null
+++ b/webook/interactive/wire_gen.go
@@ -0,0 +1,56 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/interactive/events"
+ "gitee.com/geekbang/basic-go/webook/interactive/grpc"
+ "gitee.com/geekbang/basic-go/webook/interactive/ioc"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitAPP() *App {
+ loggerV1 := ioc.InitLogger()
+ srcDB := ioc.InitSRC(loggerV1)
+ dstDB := ioc.InitDST(loggerV1)
+ doubleWritePool := ioc.InitDoubleWritePool(srcDB, dstDB)
+ db := ioc.InitBizDB(doubleWritePool)
+ interactiveDAO := dao.NewGORMInteractiveDAO(db)
+ cmdable := ioc.InitRedis()
+ interactiveCache := cache.NewRedisInteractiveCache(cmdable)
+ interactiveRepository := repository.NewCachedInteractiveRepository(interactiveDAO, interactiveCache, loggerV1)
+ interactiveService := service.NewInteractiveService(interactiveRepository, loggerV1)
+ interactiveServiceServer := grpc.NewInteractiveServiceServer(interactiveService)
+ server := ioc.InitGRPCxServer(loggerV1, interactiveServiceServer)
+ client := ioc.InitKafka()
+ interactiveReadEventConsumer := events.NewInteractiveReadEventConsumer(client, loggerV1, interactiveRepository)
+ consumer := ioc.InitFixDataConsumer(loggerV1, srcDB, dstDB, client)
+ v := ioc.NewConsumers(interactiveReadEventConsumer, consumer)
+ syncProducer := ioc.InitSyncProducer(client)
+ producer := ioc.InitMigradatorProducer(syncProducer)
+ ginxServer := ioc.InitMigratorWeb(loggerV1, srcDB, dstDB, doubleWritePool, producer)
+ app := &App{
+ server: server,
+ consumers: v,
+ webAdmin: ginxServer,
+ }
+ return app
+}
+
+// wire.go:
+
+var thirdPartySet = wire.NewSet(ioc.InitDST, ioc.InitSRC, ioc.InitBizDB, ioc.InitDoubleWritePool, ioc.InitLogger, ioc.InitKafka, ioc.InitSyncProducer, ioc.InitRedis)
+
+var interactiveSvcProvider = wire.NewSet(service.NewInteractiveService, repository.NewCachedInteractiveRepository, dao.NewGORMInteractiveDAO, cache.NewRedisInteractiveCache)
+
+var migratorProvider = wire.NewSet(ioc.InitMigratorWeb, ioc.InitFixDataConsumer, ioc.InitMigradatorProducer)
diff --git a/webook/internal/domain/article.go b/webook/internal/domain/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..5f11f012bbfec77909183751e848e30c67e9f396
--- /dev/null
+++ b/webook/internal/domain/article.go
@@ -0,0 +1,96 @@
+package domain
+
+import "time"
+
+// Article 可以同时表达线上库和制作库的概念吗?
+// 可以同时表达,作者眼中的 Article 和读者眼中的 Article 吗?
+type Article struct {
+ Id int64
+ Title string
+ Content string
+ // Author 要从用户来
+ Author Author
+ Status ArticleStatus
+ Ctime time.Time
+ Utime time.Time
+
+ // 做成这样,就应该在 service 或者 repository 里面完成构造
+ // 设计成这个样子,就认为 Interactive 是 Article 的一个属性(值对象)
+ // Intr Interactive
+ //
+}
+
+func (a *Article) MarkedAsPrivate() {
+ a.Status = ArticleStatusPrivate
+}
+
+//func (a *Article) Publish() {
+// a.repo.Save(a)
+// a.liveRepo.Save(a)
+//}
+
+func (a Article) Abstract() string {
+ // 摘要我们取前几句。
+ // 要考虑一个中文问题
+ cs := []rune(a.Content)
+ if len(cs) < 100 {
+ return a.Content
+ }
+ // 英文怎么截取一个完整的单词,我的看法是……不需要纠结,就截断拉到
+ // 词组、介词,往后找标点符号
+ return string(cs[:100])
+}
+
+type ArticleStatus uint8
+
+const (
+ // ArticleStatusUnknown 为了避免零值之类的问题
+ ArticleStatusUnknown ArticleStatus = iota
+ ArticleStatusUnpublished
+ ArticleStatusPublished
+ ArticleStatusPrivate
+)
+
+func (s ArticleStatus) ToUint8() uint8 {
+ return uint8(s)
+}
+
+func (s ArticleStatus) NonPublished() bool {
+ return s != ArticleStatusPublished
+}
+
+func (s ArticleStatus) String() string {
+ switch s {
+ case ArticleStatusPrivate:
+ return "private"
+ case ArticleStatusUnpublished:
+ return "unpublished"
+ case ArticleStatusPublished:
+ return "published"
+ default:
+ return "unknown"
+ }
+}
+
+// ArticleStatusV1 如果你的状态很复杂,有很多行为(就是你要搞很多方法),状态里面需要一些额外字段
+// 就用这个版本
+type ArticleStatusV1 struct {
+ Val uint8
+ Name string
+}
+
+var (
+ ArticleStatusV1Unknown = ArticleStatusV1{Val: 0, Name: "unknown"}
+)
+
+type ArticleStatusV2 string
+
+// Author 在帖子这个领域内,是一个值对象
+type Author struct {
+ Id int64
+ Name string
+}
+
+//type AuthorV1 struct {
+// articles []Article
+//}
diff --git a/webook/internal/domain/job.go b/webook/internal/domain/job.go
new file mode 100644
index 0000000000000000000000000000000000000000..acc2f3d36c2d9a22aa2cdb7e37488ef2b719753a
--- /dev/null
+++ b/webook/internal/domain/job.go
@@ -0,0 +1,31 @@
+package domain
+
+import (
+ "github.com/robfig/cron/v3"
+ "time"
+)
+
+type Job struct {
+ Id int64
+ // 比如说 ranking
+ Name string
+
+ Cron string
+ Executor string
+ // 通用的任务的抽象,我们也不知道任务的具体细节,所以就搞一个 Cfg
+ // 具体任务设置具体的值
+ Cfg string
+
+ CancelFunc func() error
+}
+
+var parser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom |
+ cron.Month | cron.Dow | cron.Descriptor)
+
+func (j Job) NextTime() time.Time {
+ // 你怎么算?要根据 cron 表达式来算
+ // 可以做成包变量,因为基本不可能变
+
+ s, _ := parser.Parse(j.Cron)
+ return s.Next(time.Now())
+}
diff --git a/webook/internal/domain/resource.go b/webook/internal/domain/resource.go
new file mode 100644
index 0000000000000000000000000000000000000000..541e206ecb3b0dd5d010c4bfe904f20150da34da
--- /dev/null
+++ b/webook/internal/domain/resource.go
@@ -0,0 +1,8 @@
+package domain
+
+type Resource struct {
+ Biz string
+ BizId int64
+}
+
+const BizArticle = "article"
diff --git a/webook/internal/domain/user.go b/webook/internal/domain/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..0c990037997b6383782fcc78a0fc5e147039c52c
--- /dev/null
+++ b/webook/internal/domain/user.go
@@ -0,0 +1,23 @@
+package domain
+
+import (
+ "time"
+)
+
+// User 领域对象,是 DDD 中的 entity
+// BO(business object)
+type User struct {
+ Id int64
+ Email string
+ //
+ Password string `fe:"input=password"`
+ Phone string
+ Nickname string
+
+ // 不要组合,万一你将来可能还有 DingDingInfo,里面有同名字段 UnionID
+ WechatInfo WechatInfo
+ Ctime time.Time
+}
+
+//type Address struct {
+//}
diff --git a/webook/internal/domain/wechat.go b/webook/internal/domain/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..720bcc3ff3b58e8f734935ff18bff9867c3ed71e
--- /dev/null
+++ b/webook/internal/domain/wechat.go
@@ -0,0 +1,6 @@
+package domain
+
+type WechatInfo struct {
+ OpenID string
+ UnionID string
+}
diff --git a/webook/internal/errs/code.go b/webook/internal/errs/code.go
new file mode 100644
index 0000000000000000000000000000000000000000..4ba998e79852d5eaafe116189ce466f90b62a41d
--- /dev/null
+++ b/webook/internal/errs/code.go
@@ -0,0 +1,35 @@
+package errs
+
+const (
+ // CommonInvalidInput 任何模块都可以使用的表达输入错误
+ CommonInvalidInput = 400001
+ CommonInternalServer = 500001
+)
+
+// 用户模块
+const (
+ // UserInvalidInput 用户模块输入错误,这是一个含糊的错误
+ UserInvalidInput = 401001
+ UserInternalServerError = 501001
+ // UserInvalidOrPassword 用户不存在或者密码错误,这个你要小心,
+ // 防止有人跟你过不去
+ UserInvalidOrPassword = 401002
+)
+
+const (
+ ArticleInvalidInput = 402001
+ ArticleInternalServerError = 502001
+)
+
+var (
+ // UserInvalidInputV1 这个东西是你 DEBUG 用的,不是给 C 端用户用的
+ UserInvalidInputV1 = Code{
+ Number: 401001,
+ Msg: "用户输入错误",
+ }
+)
+
+type Code struct {
+ Number int
+ Msg string
+}
diff --git a/webook/internal/events/article/history_record.go b/webook/internal/events/article/history_record.go
new file mode 100644
index 0000000000000000000000000000000000000000..2bdde0335fffe8112a53e2ca5269de0ad4153c28
--- /dev/null
+++ b/webook/internal/events/article/history_record.go
@@ -0,0 +1,47 @@
+package article
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+)
+
+type HistoryReadEventConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+}
+
+func NewHistoryReadEventConsumer(
+ client sarama.Client,
+ l logger.LoggerV1) *HistoryReadEventConsumer {
+ return &HistoryReadEventConsumer{
+ client: client,
+ l: l,
+ }
+}
+
+func (r *HistoryReadEventConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("history_record",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"read_article"},
+ saramax.NewHandler[ReadEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+// Consume 这个不是幂等的
+func (r *HistoryReadEventConsumer) Consume(msg *sarama.ConsumerMessage, t ReadEvent) error {
+ //ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ //defer cancel()
+ //return r.repo.Add(ctx, t.Aid, t.Uid)
+ panic("implement me")
+}
diff --git a/webook/internal/events/article/producer.go b/webook/internal/events/article/producer.go
new file mode 100644
index 0000000000000000000000000000000000000000..6606dc8d0a6b2f61bd36ce53142f8819c8dba338
--- /dev/null
+++ b/webook/internal/events/article/producer.go
@@ -0,0 +1,51 @@
+package article
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/IBM/sarama"
+)
+
+type Producer interface {
+ ProduceReadEvent(ctx context.Context, evt ReadEvent) error
+ ProduceReadEventV1(ctx context.Context, v1 ReadEventV1)
+}
+
+type KafkaProducer struct {
+ producer sarama.SyncProducer
+}
+
+func (k *KafkaProducer) ProduceReadEventV1(ctx context.Context, v1 ReadEventV1) {
+ //TODO implement me
+ panic("implement me")
+}
+
+// ProduceReadEvent 如果你有复杂的重试逻辑,就用装饰器
+// 你认为你的重试逻辑很简单,你就放这里
+func (k *KafkaProducer) ProduceReadEvent(ctx context.Context, evt ReadEvent) error {
+ data, err := json.Marshal(evt)
+ if err != nil {
+ return err
+ }
+ _, _, err = k.producer.SendMessage(&sarama.ProducerMessage{
+ Topic: "read_article",
+ Value: sarama.ByteEncoder(data),
+ })
+ return err
+}
+
+func NewKafkaProducer(pc sarama.SyncProducer) Producer {
+ return &KafkaProducer{
+ producer: pc,
+ }
+}
+
+type ReadEvent struct {
+ Uid int64
+ Aid int64
+}
+
+type ReadEventV1 struct {
+ Uids []int64
+ Aids []int64
+}
diff --git a/webook/internal/events/mysql_binlog_event.go b/webook/internal/events/mysql_binlog_event.go
new file mode 100644
index 0000000000000000000000000000000000000000..5dd592a97fdf26b54f64aeecb31868740450c7c1
--- /dev/null
+++ b/webook/internal/events/mysql_binlog_event.go
@@ -0,0 +1,65 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ dao "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/pkg/canalx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+type MySQLBinlogConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+ // 耦合到实现,而不是耦合到接口,除非你把操作缓存的方法也定义到 repository 接口上。
+ repo *article.CachedArticleRepository
+}
+
+func (r *MySQLBinlogConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("pub_articles_cache",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"webook_binlog"},
+ // 这里逼不得已和 DAO 耦合在了一起
+ saramax.NewHandler[canalx.Message[dao.PublishedArticle]](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (r *MySQLBinlogConsumer) Consume(msg *sarama.ConsumerMessage,
+ cmsg canalx.Message[dao.PublishedArticle]) error {
+ // 别的表的 binlog,你不关心
+ // 可以考虑,不同的表用不同的 topic,那么你这里就不需要判定了
+ if cmsg.Table != "published_articles" {
+ return nil
+ }
+ // 要在这里更新缓存了
+ // 增删改的消息,实际上在 publish article 里面是没有删的消息的
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ for _, data := range cmsg.Data {
+ var err error
+ switch data.Status {
+ case domain.ArticleStatusPublished.ToUint8():
+ // 发表,要写入缓存
+ err = r.repo.Cache().SetPub(ctx, r.repo.ToDomain(dao.Article(data)))
+ case domain.ArticleStatusPrivate.ToUint8():
+ err = r.repo.Cache().DelPub(ctx, data.Id)
+ }
+ if err != nil {
+ // 正常记录一下日志就行
+ }
+ }
+ return nil
+}
diff --git a/webook/internal/events/types.go b/webook/internal/events/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec4622544c2dc70b04f57c12375836b106c41d88
--- /dev/null
+++ b/webook/internal/events/types.go
@@ -0,0 +1,5 @@
+package events
+
+type Consumer interface {
+ Start() error
+}
diff --git a/webook/internal/integration/article_gorm_test.go b/webook/internal/integration/article_gorm_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..5e73b22fa3510b2d36a3729166083d0ce4f0bc8d
--- /dev/null
+++ b/webook/internal/integration/article_gorm_test.go
@@ -0,0 +1,432 @@
+//go:build e2e
+
+package integration
+
+import (
+ "bytes"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+type ArticleGORMHandlerTestSuite struct {
+ suite.Suite
+ server *gin.Engine
+ db *gorm.DB
+}
+
+func (s *ArticleGORMHandlerTestSuite) SetupSuite() {
+ s.server = gin.Default()
+ s.server.Use(func(context *gin.Context) {
+ // 直接设置好
+ context.Set("users", &ijwt.UserClaims{
+ Id: 123,
+ })
+ context.Next()
+ })
+ s.db = startup.InitTestDB()
+ hdl := startup.InitArticleHandler(article.NewGORMArticleDAO(s.db))
+ hdl.RegisterRoutes(s.server)
+}
+
+func (s *ArticleGORMHandlerTestSuite) TearDownTest() {
+ err := s.db.Exec("TRUNCATE TABLE `articles`").Error
+ assert.NoError(s.T(), err)
+ s.db.Exec("TRUNCATE TABLE `published_articles`")
+}
+
+func (s *ArticleGORMHandlerTestSuite) TestArticleHandler_Edit() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ // 要提前准备数据
+ before func(t *testing.T)
+ // 验证并且删除数据
+ after func(t *testing.T)
+ // 构造请求,直接使用 req
+ // 也就是说,我们放弃测试 Bind 的异常分支
+ req Article
+
+ // 预期响应
+ wantCode int
+ wantResult Result[int64]
+ }{
+ {
+ name: "新建帖子",
+ before: func(t *testing.T) {
+ // 什么也不需要做
+ },
+ after: func(t *testing.T) {
+ // 验证一下数据
+ var art article.Article
+ s.db.Where("author_id = ?", 123).First(&art)
+ assert.True(t, art.Ctime > 0)
+ assert.True(t, art.Utime > 0)
+ // 重置了这些值,因为无法比较
+ art.Utime = 0
+ art.Ctime = 0
+ assert.Equal(t, article.Article{
+ Id: 1,
+ Title: "hello,你好",
+ Content: "随便试试",
+ AuthorId: 123,
+ Status: domain.ArticleStatusUnpublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Title: "hello,你好",
+ Content: "随便试试",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 1,
+ },
+ },
+ {
+ // 这个是已经有了,然后修改之后再保存
+ name: "更新帖子",
+ before: func(t *testing.T) {
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ s.db.Create(&article.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ })
+ },
+ after: func(t *testing.T) {
+ // 验证一下数据
+ var art article.Article
+ s.db.Where("id = ?", 2).First(&art)
+ assert.True(t, art.Utime > 234)
+ art.Utime = 0
+ assert.Equal(t, article.Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ AuthorId: 123,
+ // 创建时间没变
+ Ctime: 456,
+ Status: domain.ArticleStatusUnpublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 2,
+ },
+ },
+
+ {
+ name: "更新别人的帖子",
+ before: func(t *testing.T) {
+ // 模拟已经存在的帖子
+ s.db.Create(&article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ // 注意。这个 AuthorID 我们设置为另外一个人的ID
+ AuthorId: 789,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ })
+ },
+ after: func(t *testing.T) {
+ // 更新应该是失败了,数据没有发生变化
+ var art article.Article
+ s.db.Where("id = ?", 3).First(&art)
+ assert.Equal(t, article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 789,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Id: 3,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ data, err := json.Marshal(tc.req)
+ // 不能有 error
+ assert.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/articles/edit", bytes.NewReader(data))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type",
+ "application/json")
+ recorder := httptest.NewRecorder()
+
+ s.server.ServeHTTP(recorder, req)
+ code := recorder.Code
+ assert.Equal(t, tc.wantCode, code)
+ if code != http.StatusOK {
+ return
+ }
+ // 反序列化为结果
+ // 利用泛型来限定结果必须是 int64
+ var result Result[int64]
+ err = json.Unmarshal(recorder.Body.Bytes(), &result)
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResult, result)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *ArticleGORMHandlerTestSuite) TestArticle_Publish() {
+ t := s.T()
+
+ testCases := []struct {
+ name string
+ // 要提前准备数据
+ before func(t *testing.T)
+ // 验证并且删除数据
+ after func(t *testing.T)
+ req Article
+
+ // 预期响应
+ wantCode int
+ wantResult Result[int64]
+ }{
+ {
+ name: "新建帖子并发表",
+ before: func(t *testing.T) {
+ // 什么也不需要做
+ },
+ after: func(t *testing.T) {
+ // 验证一下数据
+ var art article.Article
+ s.db.Where("author_id = ?", 123).First(&art)
+ assert.Equal(t, "hello,你好", art.Title)
+ assert.Equal(t, "随便试试", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ assert.True(t, art.Ctime > 0)
+ assert.True(t, art.Utime > 0)
+ var publishedArt article.PublishedArticle
+ s.db.Where("author_id = ?", 123).First(&publishedArt)
+ assert.Equal(t, "hello,你好", publishedArt.Title)
+ assert.Equal(t, "随便试试", publishedArt.Content)
+ assert.Equal(t, int64(123), publishedArt.AuthorId)
+ assert.True(t, publishedArt.Ctime > 0)
+ assert.True(t, publishedArt.Utime > 0)
+ },
+ req: Article{
+ Title: "hello,你好",
+ Content: "随便试试",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 1,
+ },
+ },
+ {
+ // 制作库有,但是线上库没有
+ name: "更新帖子并新发表",
+ before: func(t *testing.T) {
+ // 模拟已经存在的帖子
+ s.db.Create(&article.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ })
+ },
+ after: func(t *testing.T) {
+ // 验证一下数据
+ var art article.Article
+ s.db.Where("id = ?", 2).First(&art)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), art.Ctime)
+ // 更新时间变了
+ assert.True(t, art.Utime > 234)
+ var publishedArt article.PublishedArticle
+ s.db.Where("id = ?", 2).First(&publishedArt)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ assert.True(t, publishedArt.Ctime > 0)
+ assert.True(t, publishedArt.Utime > 0)
+ },
+ req: Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 2,
+ },
+ },
+ {
+ name: "更新帖子,并且重新发表",
+ before: func(t *testing.T) {
+ art := article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ }
+ s.db.Create(&art)
+ part := article.PublishedArticle(art)
+ s.db.Create(&part)
+ },
+ after: func(t *testing.T) {
+ var art article.Article
+ s.db.Where("id = ?", 3).First(&art)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), art.Ctime)
+ // 更新时间变了
+ assert.True(t, art.Utime > 234)
+
+ var part article.PublishedArticle
+ s.db.Where("id = ?", 3).First(&part)
+ assert.Equal(t, "新的标题", part.Title)
+ assert.Equal(t, "新的内容", part.Content)
+ assert.Equal(t, int64(123), part.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), part.Ctime)
+ // 更新时间变了
+ assert.True(t, part.Utime > 234)
+ },
+ req: Article{
+ Id: 3,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 3,
+ },
+ },
+ {
+ name: "更新别人的帖子,并且发表失败",
+ before: func(t *testing.T) {
+ art := article.Article{
+ Id: 4,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ // 注意。这个 AuthorID 我们设置为另外一个人的ID
+ AuthorId: 789,
+ }
+ s.db.Create(&art)
+ part := article.PublishedArticle(article.Article{
+ Id: 4,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 789,
+ })
+ s.db.Create(&part)
+ },
+ after: func(t *testing.T) {
+ // 更新应该是失败了,数据没有发生变化
+ var art article.Article
+ s.db.Where("id = ?", 4).First(&art)
+ assert.Equal(t, "我的标题", art.Title)
+ assert.Equal(t, "我的内容", art.Content)
+ assert.Equal(t, int64(456), art.Ctime)
+ assert.Equal(t, int64(234), art.Utime)
+ assert.Equal(t, int64(789), art.AuthorId)
+
+ var part article.PublishedArticle
+ // 数据没有变化
+ s.db.Where("id = ?", 4).First(&part)
+ assert.Equal(t, "我的标题", part.Title)
+ assert.Equal(t, "我的内容", part.Content)
+ assert.Equal(t, int64(789), part.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), part.Ctime)
+ // 更新时间变了
+ assert.Equal(t, int64(234), part.Utime)
+ },
+ req: Article{
+ Id: 4,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ data, err := json.Marshal(tc.req)
+ // 不能有 error
+ assert.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/articles/publish", bytes.NewReader(data))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type",
+ "application/json")
+ recorder := httptest.NewRecorder()
+
+ s.server.ServeHTTP(recorder, req)
+ code := recorder.Code
+ assert.Equal(t, tc.wantCode, code)
+ if code != http.StatusOK {
+ return
+ }
+ // 反序列化为结果
+ // 利用泛型来限定结果必须是 int64
+ var result Result[int64]
+ err = json.Unmarshal(recorder.Body.Bytes(), &result)
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResult, result)
+ tc.after(t)
+ })
+ }
+}
+
+func TestGORMArticle(t *testing.T) {
+ suite.Run(t, new(ArticleGORMHandlerTestSuite))
+}
diff --git a/webook/internal/integration/article_mongo_test.go b/webook/internal/integration/article_mongo_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7d275351a0cd1c46531c1f4483c8ba93e9248386
--- /dev/null
+++ b/webook/internal/integration/article_mongo_test.go
@@ -0,0 +1,527 @@
+//go:build e2e
+
+package integration
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/bwmarrin/snowflake"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+type ArticleMongoHandlerTestSuite struct {
+ suite.Suite
+ server *gin.Engine
+ mdb *mongo.Database
+ col *mongo.Collection
+ liveCol *mongo.Collection
+}
+
+func (s *ArticleMongoHandlerTestSuite) SetupSuite() {
+ s.server = gin.Default()
+ s.server.Use(func(context *gin.Context) {
+ // 直接设置好
+ context.Set("users", &ijwt.UserClaims{
+ Id: 123,
+ })
+ context.Next()
+ })
+ s.mdb = startup.InitMongoDB()
+ node, err := snowflake.NewNode(1)
+ assert.NoError(s.T(), err)
+ err = article.InitCollections(s.mdb)
+ if err != nil {
+ panic(err)
+ }
+ s.col = s.mdb.Collection("articles")
+ s.liveCol = s.mdb.Collection("published_articles")
+ hdl := startup.InitArticleHandler(article.NewMongoDBDAO(s.mdb, node))
+ hdl.RegisterRoutes(s.server)
+}
+
+func (s *ArticleMongoHandlerTestSuite) TearDownTest() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ _, err := s.mdb.Collection("articles").
+ DeleteMany(ctx, bson.D{})
+ assert.NoError(s.T(), err)
+ _, err = s.mdb.Collection("published_articles").
+ DeleteMany(ctx, bson.D{})
+ assert.NoError(s.T(), err)
+}
+
+func (s *ArticleMongoHandlerTestSuite) TestCleanMongo() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ _, err := s.mdb.Collection("articles").
+ DeleteMany(ctx, bson.D{})
+ assert.NoError(s.T(), err)
+ _, err = s.mdb.Collection("published_articles").
+ DeleteMany(ctx, bson.D{})
+ assert.NoError(s.T(), err)
+}
+
+func (s *ArticleMongoHandlerTestSuite) TestArticleHandler_Edit() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ // 要提前准备数据
+ before func(t *testing.T)
+ // 验证并且删除数据
+ after func(t *testing.T)
+ // 构造请求,直接使用 req
+ // 也就是说,我们放弃测试 Bind 的异常分支
+ req Article
+
+ // 预期响应
+ wantCode int
+ wantResult Result[int64]
+ }{
+ {
+ name: "新建帖子",
+ before: func(t *testing.T) {
+ // 什么也不需要做
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{"author_id", 123}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.True(t, art.Ctime > 0)
+ assert.True(t, art.Utime > 0)
+ // 我们断定 ID 生成了
+ assert.True(t, art.Id > 0)
+ // 重置了这些值,因为无法比较
+ art.Utime = 0
+ art.Ctime = 0
+ art.Id = 0
+ assert.Equal(t, article.Article{
+ Title: "hello,你好",
+ Content: "随便试试",
+ AuthorId: 123,
+ Status: domain.ArticleStatusUnpublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Title: "hello,你好",
+ Content: "随便试试",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 1,
+ },
+ },
+
+ {
+ // 这个是已经有了,然后修改之后再保存
+ name: "更新帖子",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ _, err := s.col.InsertOne(ctx, &article.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ })
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 2}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.True(t, art.Utime > 234)
+ art.Utime = 0
+ assert.Equal(t, article.Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ AuthorId: 123,
+ // 创建时间没变
+ Ctime: 456,
+ Status: domain.ArticleStatusUnpublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 2,
+ },
+ },
+ {
+ name: "更新别人的帖子",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ _, err := s.col.InsertOne(ctx, &article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ // 注意。这个 AuthorID 我们设置为另外一个人的ID
+ AuthorId: 789,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ })
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ // 更新应该是失败了,数据没有发生变化
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 3}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.Equal(t, article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 789,
+ Status: domain.ArticleStatusPublished.ToUint8(),
+ }, art)
+ },
+ req: Article{
+ Id: 3,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ data, err := json.Marshal(tc.req)
+ // 不能有 error
+ assert.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/articles/edit", bytes.NewReader(data))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type",
+ "application/json")
+ recorder := httptest.NewRecorder()
+
+ s.server.ServeHTTP(recorder, req)
+ code := recorder.Code
+ assert.Equal(t, tc.wantCode, code)
+ if code != http.StatusOK {
+ return
+ }
+ // 反序列化为结果
+ // 利用泛型来限定结果必须是 int64
+ var result Result[int64]
+ err = json.Unmarshal(recorder.Body.Bytes(), &result)
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResult.Code, result.Code)
+ // 只能判定有 ID,因为雪花算法你无法确定具体的值
+ if tc.wantResult.Data > 0 {
+ assert.True(t, result.Data > 0)
+ }
+ tc.after(t)
+ })
+ }
+}
+
+func (s *ArticleMongoHandlerTestSuite) TestArticle_Publish() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ // 要提前准备数据
+ before func(t *testing.T)
+ // 验证并且删除数据
+ after func(t *testing.T)
+ req Article
+
+ // 预期响应
+ wantCode int
+ wantResult Result[int64]
+ }{
+ {
+ name: "新建帖子并发表",
+ before: func(t *testing.T) {
+ // 什么也不需要做
+ },
+ after: func(t *testing.T) {
+ // 验证一下数据
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "author_id", Value: 123}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.True(t, art.Id > 0)
+ assert.Equal(t, "hello,你好", art.Title)
+ assert.Equal(t, "随便试试", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ assert.True(t, art.Ctime > 0)
+ assert.True(t, art.Utime > 0)
+ var publishedArt article.PublishedArticle
+ err = s.liveCol.FindOne(ctx, bson.D{bson.E{Key: "author_id", Value: 123}}).Decode(&publishedArt)
+ assert.NoError(t, err)
+ assert.True(t, publishedArt.Id > 0)
+ assert.Equal(t, "hello,你好", publishedArt.Title)
+ assert.Equal(t, "随便试试", publishedArt.Content)
+ assert.Equal(t, int64(123), publishedArt.AuthorId)
+ assert.True(t, publishedArt.Ctime > 0)
+ assert.True(t, publishedArt.Utime > 0)
+ },
+ req: Article{
+ Title: "hello,你好",
+ Content: "随便试试",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 1,
+ },
+ },
+ {
+ // 制作库有,但是线上库没有
+ name: "更新帖子并新发表",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ _, err := s.col.InsertOne(ctx, &article.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ })
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 2}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(2), art.Id)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), art.Ctime)
+ // 更新时间变了
+ assert.True(t, art.Utime > 234)
+ var publishedArt article.PublishedArticle
+ err = s.liveCol.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 2}}).Decode(&publishedArt)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(2), art.Id)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ assert.True(t, publishedArt.Ctime > 0)
+ assert.True(t, publishedArt.Utime > 0)
+ },
+ req: Article{
+ Id: 2,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 2,
+ },
+ },
+ {
+ name: "更新帖子,并且重新发表",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ art := article.Article{
+ Id: 3,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ AuthorId: 123,
+ }
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ _, err := s.col.InsertOne(ctx, &art)
+ assert.NoError(t, err)
+ part := article.PublishedArticle(art)
+ _, err = s.liveCol.InsertOne(ctx, &part)
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 3}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3), art.Id)
+ assert.Equal(t, "新的标题", art.Title)
+ assert.Equal(t, "新的内容", art.Content)
+ assert.Equal(t, int64(123), art.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), art.Ctime)
+ // 更新时间变了
+ assert.True(t, art.Utime > 234)
+
+ var part article.PublishedArticle
+ err = s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 3}}).Decode(&part)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3), part.Id)
+ assert.Equal(t, "新的标题", part.Title)
+ assert.Equal(t, "新的内容", part.Content)
+ assert.Equal(t, int64(123), part.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), part.Ctime)
+ // 更新时间变了
+ assert.True(t, part.Utime > 234)
+ },
+ req: Article{
+ Id: 3,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Data: 3,
+ },
+ },
+ {
+ name: "更新别人的帖子,并且发表失败",
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ art := article.Article{
+ Id: 4,
+ Title: "我的标题",
+ Content: "我的内容",
+ Ctime: 456,
+ Utime: 234,
+ // 注意。这个 AuthorID 我们设置为另外一个人的ID
+ AuthorId: 789,
+ }
+ // 模拟已经存在的帖子,并且是已经发布的帖子
+ _, err := s.col.InsertOne(ctx, &art)
+ assert.NoError(t, err)
+ part := article.PublishedArticle(art)
+ _, err = s.liveCol.InsertOne(ctx, &part)
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ // 更新应该是失败了,数据没有发生变化
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证一下数据
+ var art article.Article
+ err := s.col.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 4}}).Decode(&art)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), art.Id)
+ assert.Equal(t, "我的标题", art.Title)
+ assert.Equal(t, "我的内容", art.Content)
+ assert.Equal(t, int64(456), art.Ctime)
+ assert.Equal(t, int64(234), art.Utime)
+ assert.Equal(t, int64(789), art.AuthorId)
+
+ var part article.PublishedArticle
+ // 数据没有变化
+ err = s.liveCol.FindOne(ctx, bson.D{bson.E{Key: "id", Value: 4}}).Decode(&part)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), part.Id)
+ assert.Equal(t, "我的标题", part.Title)
+ assert.Equal(t, "我的内容", part.Content)
+ assert.Equal(t, int64(789), part.AuthorId)
+ // 创建时间没变
+ assert.Equal(t, int64(456), part.Ctime)
+ // 更新时间变了
+ assert.Equal(t, int64(234), part.Utime)
+ },
+ req: Article{
+ Id: 4,
+ Title: "新的标题",
+ Content: "新的内容",
+ },
+ wantCode: 200,
+ wantResult: Result[int64]{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ data, err := json.Marshal(tc.req)
+ // 不能有 error
+ assert.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost,
+ "/articles/publish", bytes.NewReader(data))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type",
+ "application/json")
+ recorder := httptest.NewRecorder()
+
+ s.server.ServeHTTP(recorder, req)
+ code := recorder.Code
+ assert.Equal(t, tc.wantCode, code)
+ if code != http.StatusOK {
+ return
+ }
+ // 反序列化为结果
+ // 利用泛型来限定结果必须是 int64
+ var result Result[int64]
+ err = json.Unmarshal(recorder.Body.Bytes(), &result)
+ assert.NoError(t, err)
+ assert.NoError(t, err)
+ assert.Equal(t, tc.wantResult.Code, result.Code)
+ // 只能判定有 ID,因为雪花算法你无法确定具体的值
+ if tc.wantResult.Data > 0 {
+ assert.True(t, result.Data > 0)
+ }
+ tc.after(t)
+ })
+ }
+}
+
+func TestMongoArticle(t *testing.T) {
+ suite.Run(t, new(ArticleMongoHandlerTestSuite))
+}
+
+type Article struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+}
diff --git a/webook/internal/integration/result.go b/webook/internal/integration/result.go
new file mode 100644
index 0000000000000000000000000000000000000000..530d4126a20db259af4a18fa468dd597de23a0ca
--- /dev/null
+++ b/webook/internal/integration/result.go
@@ -0,0 +1,7 @@
+package integration
+
+type Result[T any] struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data T `json:"data"`
+}
diff --git a/webook/internal/integration/startup/db.go b/webook/internal/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..1bacae71acd3cc8e4d2b53fcb90333263ff505fb
--- /dev/null
+++ b/webook/internal/integration/startup/db.go
@@ -0,0 +1,71 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "go.mongodb.org/mongo-driver/event"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+var mongoDB *mongo.Database
+
+func InitMongoDB() *mongo.Database {
+ if mongoDB == nil {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ monitor := &event.CommandMonitor{
+ Started: func(ctx context.Context,
+ startedEvent *event.CommandStartedEvent) {
+ fmt.Println(startedEvent.Command)
+ },
+ }
+ opts := options.Client().
+ ApplyURI("mongodb://root:example@localhost:27017/").
+ SetMonitor(monitor)
+ client, err := mongo.Connect(ctx, opts)
+ if err != nil {
+ panic(err)
+ }
+ mongoDB = client.Database("webook")
+ }
+ return mongoDB
+}
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTable(db)
+ if err != nil {
+ panic(err)
+ }
+ db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/internal/integration/startup/init.go b/webook/internal/integration/startup/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..52a45a496390f4b2355feda83cf9420b00e75094
--- /dev/null
+++ b/webook/internal/integration/startup/init.go
@@ -0,0 +1,7 @@
+package startup
+
+import "github.com/gin-gonic/gin"
+
+func init() {
+ gin.SetMode(gin.ReleaseMode)
+}
diff --git a/webook/internal/integration/startup/init_wechat_service.go b/webook/internal/integration/startup/init_wechat_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..a18c7110d426c7c291ac137c10c53c0eda4cf9d6
--- /dev/null
+++ b/webook/internal/integration/startup/init_wechat_service.go
@@ -0,0 +1,11 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/service/oauth2/wechat"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+// InitPhantomWechatService 没啥用的虚拟的 wechatService
+func InitPhantomWechatService(l logger.LoggerV1) wechat.Service {
+ return wechat.NewService("", "", l)
+}
diff --git a/webook/internal/integration/startup/jwt.go b/webook/internal/integration/startup/jwt.go
new file mode 100644
index 0000000000000000000000000000000000000000..89dfffe7164d91468982a5a14565542e264dd49d
--- /dev/null
+++ b/webook/internal/integration/startup/jwt.go
@@ -0,0 +1 @@
+package startup
diff --git a/webook/internal/integration/startup/kafka.go b/webook/internal/integration/startup/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..f12ebf9b63e5eae071ac5d1dbcefade11799c254
--- /dev/null
+++ b/webook/internal/integration/startup/kafka.go
@@ -0,0 +1,23 @@
+package startup
+
+import (
+ "github.com/IBM/sarama"
+)
+
+func InitKafka() sarama.Client {
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ client, err := sarama.NewClient([]string{"localhost:9092"}, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+func NewSyncProducer(client sarama.Client) sarama.SyncProducer {
+ res, err := sarama.NewSyncProducerFromClient(client)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
diff --git a/webook/internal/integration/startup/log.go b/webook/internal/integration/startup/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..a659ad9dbf326536df6bc5e6641a4aed105b15bc
--- /dev/null
+++ b/webook/internal/integration/startup/log.go
@@ -0,0 +1,9 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+func InitLog() logger.LoggerV1 {
+ return logger.NewNoOpLogger()
+}
diff --git a/webook/internal/integration/startup/redis.go b/webook/internal/integration/startup/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4d929d011e0aa42e75aa0c6c68fb582c39c8db7
--- /dev/null
+++ b/webook/internal/integration/startup/redis.go
@@ -0,0 +1,21 @@
+package startup
+
+import (
+ "context"
+ "github.com/redis/go-redis/v9"
+)
+
+var redisClient redis.Cmdable
+
+func InitRedis() redis.Cmdable {
+ if redisClient == nil {
+ redisClient = redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ })
+
+ for err := redisClient.Ping(context.Background()).Err(); err != nil; {
+ panic(err)
+ }
+ }
+ return redisClient
+}
diff --git a/webook/internal/integration/startup/wechat.go b/webook/internal/integration/startup/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8f49b9929cf5b1dc32490915855cf2a503d8e30
--- /dev/null
+++ b/webook/internal/integration/startup/wechat.go
@@ -0,0 +1,5 @@
+package startup
+
+//func InitWechatHandlerConfig() web.WechatHandlerConfig {
+// return web.WechatHandlerConfig{}
+//}
diff --git a/webook/internal/integration/startup/wire.go b/webook/internal/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..c2196a2fd94a2d87953dbe00d1dfbe71a8e1de89
--- /dev/null
+++ b/webook/internal/integration/startup/wire.go
@@ -0,0 +1,102 @@
+//go:build wireinject
+
+package startup
+
+import (
+ repository2 "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ cache2 "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ service2 "gitee.com/geekbang/basic-go/webook/interactive/service"
+ article3 "gitee.com/geekbang/basic-go/webook/internal/events/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ article2 "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/gin-gonic/gin"
+ "github.com/google/wire"
+)
+
+var thirdProvider = wire.NewSet(InitRedis,
+ NewSyncProducer,
+ InitKafka,
+ InitTestDB, InitLog)
+var userSvcProvider = wire.NewSet(
+ dao.NewUserDAO,
+ cache.NewUserCache,
+ repository.NewUserRepository,
+ service.NewUserService)
+
+var interactiveSvcProvider = wire.NewSet(
+ service2.NewInteractiveService,
+ repository2.NewCachedInteractiveRepository,
+ dao2.NewGORMInteractiveDAO,
+ cache2.NewRedisInteractiveCache,
+)
+
+var articlSvcProvider = wire.NewSet(
+ article.NewGORMArticleDAO,
+ cache.NewRedisArticleCache,
+ article2.NewArticleRepository,
+ service.NewArticleService)
+
+func InitWebServer() *gin.Engine {
+ wire.Build(
+ thirdProvider,
+ userSvcProvider,
+ articlSvcProvider,
+ interactiveSvcProvider,
+ ioc.InitIntrGRPCClient,
+ article3.NewKafkaProducer,
+ cache.NewCodeCache,
+ repository.NewCodeRepository,
+ // service 部分
+ // 集成测试我们显式指定使用内存实现
+ ioc.InitSMSService,
+
+ // 指定啥也不干的 wechat service
+ InitPhantomWechatService,
+ service.NewCodeService,
+ // handler 部分
+ web.NewUserHandler,
+ web.NewOAuth2WechatHandler,
+ web.NewArticleHandler,
+ ijwt.NewRedisJWTHandler,
+
+ // gin 的中间件
+ ioc.InitMiddlewares,
+
+ // Web 服务器
+ ioc.InitWebServer,
+ )
+ // 随便返回一个
+ return gin.Default()
+}
+
+func InitArticleHandler(dao article.ArticleDAO) *web.ArticleHandler {
+ wire.Build(thirdProvider,
+ userSvcProvider,
+ interactiveSvcProvider,
+ cache.NewRedisArticleCache,
+ ioc.InitIntrGRPCClient,
+ //wire.InterfaceValue(new(article.ArticleDAO), dao),
+ article3.NewKafkaProducer,
+ article2.NewArticleRepository,
+ service.NewArticleService,
+ web.NewArticleHandler)
+ return new(web.ArticleHandler)
+}
+
+func InitUserSvc() service.UserService {
+ wire.Build(thirdProvider, userSvcProvider)
+ return service.NewUserService(nil, nil)
+}
+
+func InitJwtHdl() ijwt.Handler {
+ wire.Build(thirdProvider, ijwt.NewRedisJWTHandler)
+ return ijwt.NewRedisJWTHandler(nil)
+}
diff --git a/webook/internal/integration/startup/wire_gen.go b/webook/internal/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..4e71f9b461a4219dbcf342d4ec9629519ed91903
--- /dev/null
+++ b/webook/internal/integration/startup/wire_gen.go
@@ -0,0 +1,114 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ repository2 "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ cache2 "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ service2 "gitee.com/geekbang/basic-go/webook/interactive/service"
+ article3 "gitee.com/geekbang/basic-go/webook/internal/events/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ article2 "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/gin-gonic/gin"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitWebServer() *gin.Engine {
+ cmdable := InitRedis()
+ loggerV1 := InitLog()
+ handler := jwt.NewRedisJWTHandler(cmdable)
+ v := ioc.InitMiddlewares(cmdable, loggerV1, handler)
+ gormDB := InitTestDB()
+ userDAO := dao.NewUserDAO(gormDB)
+ userCache := cache.NewUserCache(cmdable)
+ userRepository := repository.NewUserRepository(userDAO, userCache)
+ userService := service.NewUserService(userRepository, loggerV1)
+ codeCache := cache.NewCodeCache(cmdable)
+ codeRepository := repository.NewCodeRepository(codeCache)
+ smsService := ioc.InitSMSService(cmdable)
+ codeService := service.NewCodeService(codeRepository, smsService)
+ userHandler := web.NewUserHandler(userService, codeService, handler)
+ wechatService := InitPhantomWechatService(loggerV1)
+ oAuth2WechatHandler := web.NewOAuth2WechatHandler(wechatService, userService, handler)
+ articleDAO := article.NewGORMArticleDAO(gormDB)
+ articleCache := cache.NewRedisArticleCache(cmdable)
+ articleRepository := article2.NewArticleRepository(articleDAO, articleCache, userRepository, loggerV1)
+ client := InitKafka()
+ syncProducer := NewSyncProducer(client)
+ producer := article3.NewKafkaProducer(syncProducer)
+ articleService := service.NewArticleService(articleRepository, loggerV1, producer)
+ interactiveDAO := dao2.NewGORMInteractiveDAO(gormDB)
+ interactiveCache := cache2.NewRedisInteractiveCache(cmdable)
+ interactiveRepository := repository2.NewCachedInteractiveRepository(interactiveDAO, interactiveCache, loggerV1)
+ interactiveService := service2.NewInteractiveService(interactiveRepository, loggerV1)
+ interactiveServiceClient := ioc.InitIntrGRPCClient(interactiveService)
+ articleHandler := web.NewArticleHandler(articleService, interactiveServiceClient, loggerV1)
+ engine := ioc.InitWebServer(v, userHandler, oAuth2WechatHandler, articleHandler)
+ return engine
+}
+
+func InitArticleHandler(dao3 article.ArticleDAO) *web.ArticleHandler {
+ cmdable := InitRedis()
+ articleCache := cache.NewRedisArticleCache(cmdable)
+ gormDB := InitTestDB()
+ userDAO := dao.NewUserDAO(gormDB)
+ userCache := cache.NewUserCache(cmdable)
+ userRepository := repository.NewUserRepository(userDAO, userCache)
+ loggerV1 := InitLog()
+ articleRepository := article2.NewArticleRepository(dao3, articleCache, userRepository, loggerV1)
+ client := InitKafka()
+ syncProducer := NewSyncProducer(client)
+ producer := article3.NewKafkaProducer(syncProducer)
+ articleService := service.NewArticleService(articleRepository, loggerV1, producer)
+ interactiveDAO := dao2.NewGORMInteractiveDAO(gormDB)
+ interactiveCache := cache2.NewRedisInteractiveCache(cmdable)
+ interactiveRepository := repository2.NewCachedInteractiveRepository(interactiveDAO, interactiveCache, loggerV1)
+ interactiveService := service2.NewInteractiveService(interactiveRepository, loggerV1)
+ interactiveServiceClient := ioc.InitIntrGRPCClient(interactiveService)
+ articleHandler := web.NewArticleHandler(articleService, interactiveServiceClient, loggerV1)
+ return articleHandler
+}
+
+func InitUserSvc() service.UserService {
+ gormDB := InitTestDB()
+ userDAO := dao.NewUserDAO(gormDB)
+ cmdable := InitRedis()
+ userCache := cache.NewUserCache(cmdable)
+ userRepository := repository.NewUserRepository(userDAO, userCache)
+ loggerV1 := InitLog()
+ userService := service.NewUserService(userRepository, loggerV1)
+ return userService
+}
+
+func InitJwtHdl() jwt.Handler {
+ cmdable := InitRedis()
+ handler := jwt.NewRedisJWTHandler(cmdable)
+ return handler
+}
+
+// wire.go:
+
+var thirdProvider = wire.NewSet(InitRedis,
+ NewSyncProducer,
+ InitKafka,
+ InitTestDB, InitLog)
+
+var userSvcProvider = wire.NewSet(dao.NewUserDAO, cache.NewUserCache, repository.NewUserRepository, service.NewUserService)
+
+var interactiveSvcProvider = wire.NewSet(service2.NewInteractiveService, repository2.NewCachedInteractiveRepository, dao2.NewGORMInteractiveDAO, cache2.NewRedisInteractiveCache)
+
+var articlSvcProvider = wire.NewSet(article.NewGORMArticleDAO, cache.NewRedisArticleCache, article2.NewArticleRepository, service.NewArticleService)
diff --git a/webook/internal/integration/user_test.go b/webook/internal/integration/user_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee59fc001e8a9cc1984efe26ebceb186d355a9cc
--- /dev/null
+++ b/webook/internal/integration/user_test.go
@@ -0,0 +1,183 @@
+package integration
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/internal/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+func TestUserHandler_e2e_SendLoginSMSCode(t *testing.T) {
+ server := startup.InitWebServer()
+ rdb := ioc.InitRedis()
+ testCases := []struct {
+ name string
+
+ // 你要考虑准备数据。
+ before func(t *testing.T)
+ // 以及验证数据 数据库的数据对不对,你 Redis 的数据对不对
+ after func(t *testing.T)
+ reqBody string
+
+ wantCode int
+ wantBody web.Result
+ }{
+ {
+ name: "发送成功",
+ before: func(t *testing.T) {
+ // 不需要,也就是 Redis 什么数据也没有
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ // 你要清理数据
+ // "phone_code:%s:%s"
+ val, err := rdb.GetDel(ctx, "phone_code:login:15212345678").Result()
+ cancel()
+ assert.NoError(t, err)
+ // 你的验证码是 6 位
+ assert.True(t, len(val) == 6)
+ },
+ reqBody: `
+{
+ "phone": "15212345678"
+}
+`,
+ wantCode: 200,
+ wantBody: web.Result{
+ Msg: "发送成功",
+ },
+ },
+ {
+ name: "发送太频繁",
+ before: func(t *testing.T) {
+ // 这个手机号码,已经有一个验证码了
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ _, err := rdb.Set(ctx, "phone_code:login:15212345678", "123456",
+ time.Minute*9+time.Second*30).Result()
+ cancel()
+ assert.NoError(t, err)
+
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ // 你要清理数据
+ // "phone_code:%s:%s"
+ val, err := rdb.GetDel(ctx, "phone_code:login:15212345678").Result()
+ cancel()
+ assert.NoError(t, err)
+ // 你的验证码是 6 位,没有被覆盖,还是123456
+ assert.Equal(t, "123456", val)
+ },
+ reqBody: `
+{
+ "phone": "15212345678"
+}
+`,
+ wantCode: 200,
+ wantBody: web.Result{
+ Msg: "发送太频繁,请稍后再试",
+ },
+ },
+ {
+ name: "系统错误",
+ before: func(t *testing.T) {
+ // 这个手机号码,已经有一个验证码了,但是没有过期时间
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ _, err := rdb.Set(ctx, "phone_code:login:15212345678", "123456", 0).Result()
+ cancel()
+ assert.NoError(t, err)
+
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ // 你要清理数据
+ // "phone_code:%s:%s"
+ val, err := rdb.GetDel(ctx, "phone_code:login:15212345678").Result()
+ cancel()
+ assert.NoError(t, err)
+ // 你的验证码是 6 位,没有被覆盖,还是123456
+ assert.Equal(t, "123456", val)
+ },
+ reqBody: `
+{
+ "phone": "15212345678"
+}
+`,
+ wantCode: 200,
+ wantBody: web.Result{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+
+ {
+ name: "手机号码为空",
+ before: func(t *testing.T) {
+ },
+ after: func(t *testing.T) {
+ },
+ reqBody: `
+{
+ "phone": ""
+}
+`,
+ wantCode: 200,
+ wantBody: web.Result{
+ Code: 4,
+ Msg: "输入有误",
+ },
+ },
+ {
+ name: "数据格式错误",
+ before: func(t *testing.T) {
+ },
+ after: func(t *testing.T) {
+ },
+ reqBody: `
+{
+ "phone": ,
+}
+`,
+ wantCode: 400,
+ //wantBody: web.Result{
+ // Code: 4,
+ // Msg: "输入有误",
+ //},
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ req, err := http.NewRequest(http.MethodPost,
+ "/users/login_sms/code/send", bytes.NewBuffer([]byte(tc.reqBody)))
+ require.NoError(t, err)
+ // 数据是 JSON 格式
+ req.Header.Set("Content-Type", "application/json")
+ // 这里你就可以继续使用 req
+
+ resp := httptest.NewRecorder()
+ // 这就是 HTTP 请求进去 GIN 框架的入口。
+ // 当你这样调用的时候,GIN 就会处理这个请求
+ // 响应写回到 resp 里
+ server.ServeHTTP(resp, req)
+
+ assert.Equal(t, tc.wantCode, resp.Code)
+ if resp.Code != 200 {
+ return
+ }
+ var webRes web.Result
+ err = json.NewDecoder(resp.Body).Decode(&webRes)
+ require.NoError(t, err)
+ assert.Equal(t, tc.wantBody, webRes)
+ tc.after(t)
+ })
+ }
+}
diff --git a/webook/internal/job/job.go b/webook/internal/job/job.go
new file mode 100644
index 0000000000000000000000000000000000000000..9b5311783f343d3aaba33ebf6e6442393a070385
--- /dev/null
+++ b/webook/internal/job/job.go
@@ -0,0 +1,6 @@
+package job
+
+type Job interface {
+ Name() string
+ Run() error
+}
diff --git a/webook/internal/job/job_builder.go b/webook/internal/job/job_builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..0fae6b2d0e4f3a95299b70091c22ad2b1c1f85f5
--- /dev/null
+++ b/webook/internal/job/job_builder.go
@@ -0,0 +1,66 @@
+package job
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/robfig/cron/v3"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/trace"
+ "strconv"
+ "time"
+)
+
+type CronJobBuilder struct {
+ l logger.LoggerV1
+ p *prometheus.SummaryVec
+ tracer trace.Tracer
+}
+
+func NewCronJobBuilder(l logger.LoggerV1) *CronJobBuilder {
+ p := prometheus.NewSummaryVec(prometheus.SummaryOpts{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Help: "统计定时任务的执行情况",
+ Name: "cron_job",
+ }, []string{"name", "success"})
+ prometheus.MustRegister(p)
+ return &CronJobBuilder{
+ l: l,
+ p: p,
+ tracer: otel.GetTracerProvider().Tracer("webook/internal/job"),
+ }
+}
+
+func (b *CronJobBuilder) Build(job Job) cron.Job {
+ name := job.Name()
+ return cronJobFuncAdapter(func() error {
+ _, span := b.tracer.Start(context.Background(), name)
+ defer span.End()
+ start := time.Now()
+ b.l.Info("任务开始",
+ logger.String("job", name))
+ var success bool
+ defer func() {
+ b.l.Info("任务结束",
+ logger.String("job", name))
+ duration := time.Since(start).Milliseconds()
+ b.p.WithLabelValues(name,
+ strconv.FormatBool(success)).Observe(float64(duration))
+ }()
+ err := job.Run()
+ success = err == nil
+ if err != nil {
+ span.RecordError(err)
+ b.l.Error("运行任务失败", logger.Error(err),
+ logger.String("job", name))
+ }
+ return nil
+ })
+}
+
+type cronJobFuncAdapter func() error
+
+func (c cronJobFuncAdapter) Run() {
+ _ = c()
+}
diff --git a/webook/internal/job/mysql_job.go b/webook/internal/job/mysql_job.go
new file mode 100644
index 0000000000000000000000000000000000000000..653b34a37e65e77fcbfcdb9fc618e46463110a78
--- /dev/null
+++ b/webook/internal/job/mysql_job.go
@@ -0,0 +1,156 @@
+package job
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "golang.org/x/sync/semaphore"
+ "net/http"
+ "time"
+)
+
+type Executor interface {
+ // Executor 叫什么
+ Name() string
+ // Exec ctx 是整个任务调度的上下文
+ // 当从 ctx.Done 有信号的时候,就需要考虑结束执行
+ // 具体实现来控制
+ // 真正去执行一个任务
+ Exec(ctx context.Context, j domain.Job) error
+}
+
+type HttpExecutor struct {
+}
+
+func (h *HttpExecutor) Name() string {
+ return "http"
+}
+
+func (h *HttpExecutor) Exec(ctx context.Context, j domain.Job) error {
+ type Config struct {
+ Endpoint string
+ Method string
+ }
+ var cfg Config
+ err := json.Unmarshal([]byte(j.Cfg), &cfg)
+ if err != nil {
+ return err
+ }
+ req, err := http.NewRequest(cfg.Method, cfg.Endpoint, nil)
+ if err != nil {
+ return err
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if resp.StatusCode != http.StatusOK {
+ return errors.New("执行失败")
+ }
+ return nil
+}
+
+type LocalFuncExecutor struct {
+ funcs map[string]func(ctx context.Context, j domain.Job) error
+ // fn func(ctx context.Context, j domain.Job)
+}
+
+func NewLocalFuncExecutor() *LocalFuncExecutor {
+ return &LocalFuncExecutor{funcs: make(map[string]func(ctx context.Context, j domain.Job) error)}
+}
+
+func (l *LocalFuncExecutor) Name() string {
+ return "local"
+}
+
+func (l *LocalFuncExecutor) RegisterFunc(name string, fn func(ctx context.Context, j domain.Job) error) {
+ l.funcs[name] = fn
+}
+
+func (l *LocalFuncExecutor) Exec(ctx context.Context, j domain.Job) error {
+ fn, ok := l.funcs[j.Name]
+ if !ok {
+ return fmt.Errorf("未知任务,你是否注册了? %s", j.Name)
+ }
+ return fn(ctx, j)
+}
+
+// Scheduler 调度器
+type Scheduler struct {
+ execs map[string]Executor
+ svc service.JobService
+ l logger.LoggerV1
+ limiter *semaphore.Weighted
+}
+
+func NewScheduler(svc service.JobService, l logger.LoggerV1) *Scheduler {
+ return &Scheduler{svc: svc, l: l,
+ limiter: semaphore.NewWeighted(200),
+ execs: make(map[string]Executor)}
+}
+
+func (s *Scheduler) RegisterExecutor(exec Executor) {
+ s.execs[exec.Name()] = exec
+}
+
+func (s *Scheduler) Schedule(ctx context.Context) error {
+ for {
+
+ if ctx.Err() != nil {
+ // 退出调度循环
+ return ctx.Err()
+ }
+ err := s.limiter.Acquire(ctx, 1)
+ if err != nil {
+ return err
+ }
+ // 一次调度的数据库查询时间
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ j, err := s.svc.Preempt(dbCtx)
+ cancel()
+ if err != nil {
+ // 你不能 return
+ // 你要继续下一轮
+ s.l.Error("抢占任务失败", logger.Error(err))
+ }
+
+ exec, ok := s.execs[j.Executor]
+ if !ok {
+ // DEBUG 的时候最好中断
+ // 线上就继续
+ s.l.Error("未找到对应的执行器",
+ logger.String("executor", j.Executor))
+ continue
+ }
+
+ // 接下来就是执行
+ // 怎么执行?
+ go func() {
+ defer func() {
+ s.limiter.Release(1)
+ err1 := j.CancelFunc()
+ if err1 != nil {
+ s.l.Error("释放任务失败",
+ logger.Error(err1),
+ logger.Int64("jid", j.Id))
+ }
+ }()
+ // 异步执行,不要阻塞主调度循环
+ // 执行完毕之后
+ // 这边要考虑超时控制,任务的超时控制
+ err1 := exec.Exec(ctx, j)
+ if err1 != nil {
+ // 你也可以考虑在这里重试
+ s.l.Error("任务执行失败", logger.Error(err1))
+ }
+ // 你要不要考虑下一次调度?
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ err1 = s.svc.ResetNextTime(ctx, j)
+ if err1 != nil {
+ s.l.Error("设置下一次执行时间失败", logger.Error(err1))
+ }
+ }()
+ }
+}
diff --git a/webook/internal/job/ranking_job.go b/webook/internal/job/ranking_job.go
new file mode 100644
index 0000000000000000000000000000000000000000..1756a510b91660564aa0df005ff5022fc39aa23d
--- /dev/null
+++ b/webook/internal/job/ranking_job.go
@@ -0,0 +1,89 @@
+package job
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ rlock "github.com/gotomicro/redis-lock"
+ "sync"
+ "time"
+)
+
+type RankingJob struct {
+ svc service.RankingService
+ timeout time.Duration
+ client *rlock.Client
+ key string
+ l logger.LoggerV1
+ lock *rlock.Lock
+ localLock *sync.Mutex
+}
+
+func NewRankingJob(svc service.RankingService,
+ client *rlock.Client,
+ l logger.LoggerV1,
+ timeout time.Duration) *RankingJob {
+ // 根据你的数据量来,如果要是七天内的帖子数量很多,你就要设置长一点
+ return &RankingJob{svc: svc,
+ timeout: timeout,
+ client: client,
+ key: "rlock:cron_job:ranking",
+ l: l,
+ localLock: &sync.Mutex{},
+ }
+}
+
+func (r *RankingJob) Name() string {
+ return "ranking"
+}
+
+// 按时间调度的,三分钟一次
+func (r *RankingJob) Run() error {
+ r.localLock.Lock()
+ defer r.localLock.Unlock()
+ if r.lock == nil {
+ // 说明你没拿到锁,你得试着拿锁
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ // 我可以设置一个比较短的过期时间
+ lock, err := r.client.Lock(ctx, r.key, r.timeout, &rlock.FixIntervalRetry{
+ Interval: time.Millisecond * 100,
+ Max: 0,
+ }, time.Second)
+ if err != nil {
+ // 这边没拿到锁,极大概率是别人持有了锁
+ return nil
+ }
+ r.lock = lock
+ // 我怎么保证我这里,一直拿着这个锁???
+ go func() {
+ // 自动续约机制
+ err1 := lock.AutoRefresh(r.timeout/2, time.Second)
+ // 这里说明退出了续约机制
+ // 续约失败了怎么办?
+ if err1 != nil {
+ // 不怎么办
+ // 争取下一次,继续抢锁
+ r.l.Error("续约失败", logger.Error(err))
+ }
+ r.localLock.Lock()
+ r.lock = nil
+ r.localLock.Unlock()
+ // lock.Unlock(ctx)
+ }()
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
+ defer cancel()
+ return r.svc.TopN(ctx)
+}
+
+func (r *RankingJob) Close() error {
+ r.localLock.Lock()
+ lock := r.lock
+ r.lock = nil
+ r.localLock.Unlock()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return lock.Unlock(ctx)
+}
diff --git a/webook/internal/job/ranking_job_test.go b/webook/internal/job/ranking_job_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8d7765aa64c14799f4f2ce035779c7c60df16038
--- /dev/null
+++ b/webook/internal/job/ranking_job_test.go
@@ -0,0 +1 @@
+package job
diff --git a/webook/internal/job/robfig_adapter.go b/webook/internal/job/robfig_adapter.go
new file mode 100644
index 0000000000000000000000000000000000000000..a4c97120fe694686cbe09f5b1414add6fe4ae961
--- /dev/null
+++ b/webook/internal/job/robfig_adapter.go
@@ -0,0 +1,36 @@
+package job
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/prometheus/client_golang/prometheus"
+ "time"
+)
+
+type RankingJobAdapter struct {
+ j Job
+ l logger.LoggerV1
+ p prometheus.Summary
+}
+
+func NewRankingJobAdapter(j Job, l logger.LoggerV1) *RankingJobAdapter {
+ p := prometheus.NewSummary(prometheus.SummaryOpts{
+ Name: "cron_job",
+ ConstLabels: map[string]string{
+ "name": j.Name(),
+ },
+ })
+ prometheus.MustRegister(p)
+ return &RankingJobAdapter{}
+}
+func (r *RankingJobAdapter) Run() {
+ start := time.Now()
+ defer func() {
+ duration := time.Since(start).Milliseconds()
+ r.p.Observe(float64(duration))
+ }()
+ err := r.j.Run()
+ if err != nil {
+ r.l.Error("运行任务失败", logger.Error(err),
+ logger.String("job", r.j.Name()))
+ }
+}
diff --git a/webook/internal/loggerxx/logger.go b/webook/internal/loggerxx/logger.go
new file mode 100644
index 0000000000000000000000000000000000000000..89d40c673097841385d4a4bf7eeb6e4b9e48c41d
--- /dev/null
+++ b/webook/internal/loggerxx/logger.go
@@ -0,0 +1,16 @@
+package loggerxx
+
+import "go.uber.org/zap"
+
+var Logger *zap.Logger
+
+func InitLogger(l *zap.Logger) {
+ Logger = l
+}
+
+// InitLoggerV1 main 函数调用一下
+func InitLoggerV1() {
+ Logger, _ = zap.NewDevelopment()
+}
+
+//var SecureLogger *zap.Logger
diff --git a/webook/internal/repository/article/article.go b/webook/internal/repository/article/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bee5d2333aac3588c421587db795142d8c4c561
--- /dev/null
+++ b/webook/internal/repository/article/article.go
@@ -0,0 +1,285 @@
+package article
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ dao "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/ecodeclub/ekit/slice"
+ "gorm.io/gorm"
+ "time"
+)
+
+// repository 还是要用来操作缓存和DAO
+// 事务概念应该在 DAO 这一层
+
+type ArticleRepository interface {
+ Create(ctx context.Context, art domain.Article) (int64, error)
+ Update(ctx context.Context, art domain.Article) error
+ // Sync 存储并同步数据
+ Sync(ctx context.Context, art domain.Article) (int64, error)
+ SyncStatus(ctx context.Context, id int64, author int64, status domain.ArticleStatus) error
+ List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error)
+ GetByID(ctx context.Context, id int64) (domain.Article, error)
+ GetPublishedById(ctx context.Context, id int64) (domain.Article, error)
+ ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error)
+
+ // 加更新缓存的方法,强限制了所有的实现都必须有缓存
+ // SetPubCache()
+ //FindById(ctx context.Context, id int64) domain.Article
+}
+
+type CachedArticleRepository struct {
+ dao dao.ArticleDAO
+ userRepo repository.UserRepository
+
+ // v1 操作两个 DAO
+ readerDAO dao.ReaderDAO
+ authorDAO dao.AuthorDAO
+
+ // 耦合了 DAO 操作的东西
+ // 正常情况下,如果你要在 repository 层面上操作事务
+ // 那么就只能利用 db 开始事务之后,创建基于事务的 DAO
+ // 或者,直接去掉 DAO 这一层,在 repository 的实现中,直接操作 db
+ db *gorm.DB
+
+ cache cache.ArticleCache
+ l logger.LoggerV1
+}
+
+func (repo *CachedArticleRepository) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error) {
+ res, err := repo.dao.ListPub(ctx, start, offset, limit)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(res, func(idx int, src dao.Article) domain.Article {
+ return repo.ToDomain(src)
+ }), nil
+}
+
+func (repo *CachedArticleRepository) GetPublishedById(
+ ctx context.Context, id int64) (domain.Article, error) {
+ // 读取线上库数据,如果你的 Content 被你放过去了 OSS 上,你就要让前端去读 Content 字段
+ art, err := repo.dao.GetPubById(ctx, id)
+ if err != nil {
+ return domain.Article{}, err
+ }
+ // 你在这边要组装 user 了,适合单体应用
+ usr, err := repo.userRepo.FindById(ctx, art.AuthorId)
+ res := domain.Article{
+ Id: art.Id,
+ Title: art.Title,
+ Status: domain.ArticleStatus(art.Status),
+ Content: art.Content,
+ Author: domain.Author{
+ Id: usr.Id,
+ Name: usr.Nickname,
+ },
+ Ctime: time.UnixMilli(art.Ctime),
+ Utime: time.UnixMilli(art.Utime),
+ }
+ return res, nil
+}
+
+func (c *CachedArticleRepository) GetByID(ctx context.Context, id int64) (domain.Article, error) {
+ data, err := c.dao.GetById(ctx, id)
+ if err != nil {
+ return domain.Article{}, err
+ }
+ return c.ToDomain(data), nil
+}
+
+func (c *CachedArticleRepository) List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error) {
+ // 你在这个地方,集成你的复杂的缓存方案
+ // 你只缓存这一页
+ if offset == 0 && limit <= 100 {
+ data, err := c.cache.GetFirstPage(ctx, uid)
+ if err == nil {
+ go func() {
+ c.preCache(ctx, data)
+ }()
+ //return data[:limit], err
+ return data, err
+ }
+ }
+ res, err := c.dao.GetByAuthor(ctx, uid, offset, limit)
+ if err != nil {
+ return nil, err
+ }
+ data := slice.Map[dao.Article, domain.Article](res, func(idx int, src dao.Article) domain.Article {
+ return c.ToDomain(src)
+ })
+ // 回写缓存的时候,可以同步,也可以异步
+ go func() {
+ err := c.cache.SetFirstPage(ctx, uid, data)
+ c.l.Error("回写缓存失败", logger.Error(err))
+ c.preCache(ctx, data)
+ }()
+ return data, nil
+}
+
+func (repo *CachedArticleRepository) ToDomain(art dao.Article) domain.Article {
+ return domain.Article{
+ Id: art.Id,
+ Title: art.Title,
+ Status: domain.ArticleStatus(art.Status),
+ Content: art.Content,
+ Author: domain.Author{
+ Id: art.AuthorId,
+ },
+ Ctime: time.UnixMilli(art.Ctime),
+ Utime: time.UnixMilli(art.Utime),
+ }
+}
+
+func (c *CachedArticleRepository) SyncStatus(ctx context.Context, id int64, author int64, status domain.ArticleStatus) error {
+ return c.dao.SyncStatus(ctx, id, author, uint8(status))
+}
+
+func (c *CachedArticleRepository) Sync(ctx context.Context, art domain.Article) (int64, error) {
+
+ id, err := c.dao.Sync(ctx, c.toEntity(art))
+ if err == nil {
+ c.cache.DelFirstPage(ctx, art.Author.Id)
+ er := c.cache.SetPub(ctx, art)
+ if er != nil {
+ // 不需要特别关心
+ // 比如说输出 WARN 日志
+ }
+ }
+ return id, err
+}
+
+func (c *CachedArticleRepository) Cache() cache.ArticleCache {
+ return c.cache
+}
+
+//func (c *CachedArticleRepository) SyncV2_1(ctx context.Context, art domain.Article) (int64, error) {
+// // 谁在控制事务,是 repository,还是DAO在控制事务?
+// c.dao.Transaction(ctx, func(txDAO dao.ArticleDAO) error {
+//
+// })
+//}
+
+// SyncV2 尝试在 repository 层面上解决事务问题
+// 确保保存到制作库和线上库同时成功,或者同时失败
+func (c *CachedArticleRepository) SyncV2(ctx context.Context, art domain.Article) (int64, error) {
+ // 开启了一个事务
+ tx := c.db.WithContext(ctx).Begin()
+ if tx.Error != nil {
+ return 0, tx.Error
+ }
+ defer tx.Rollback()
+ // 利用 tx 来构建 DAO
+ author := dao.NewAuthorDAO(tx)
+ reader := dao.NewReaderDAO(tx)
+
+ var (
+ id = art.Id
+ err error
+ )
+ artn := c.toEntity(art)
+ // 应该先保存到制作库,再保存到线上库
+ if id > 0 {
+ err = author.UpdateById(ctx, artn)
+ } else {
+ id, err = author.Insert(ctx, artn)
+ }
+ if err != nil {
+ // 执行有问题,要回滚
+ //tx.Rollback()
+ return id, err
+ }
+ // 操作线上库了,保存数据,同步过来
+ // 考虑到,此时线上库可能有,可能没有,你要有一个 UPSERT 的写法
+ // INSERT or UPDATE
+ // 如果数据库有,那么就更新,不然就插入
+ err = reader.UpsertV2(ctx, dao.PublishedArticle(artn))
+ // 执行成功,直接提交
+ tx.Commit()
+ return id, err
+
+}
+
+func (c *CachedArticleRepository) SyncV1(ctx context.Context, art domain.Article) (int64, error) {
+ var (
+ id = art.Id
+ err error
+ )
+ artn := c.toEntity(art)
+ // 应该先保存到制作库,再保存到线上库
+ if id > 0 {
+ err = c.authorDAO.UpdateById(ctx, artn)
+ } else {
+ id, err = c.authorDAO.Insert(ctx, artn)
+ }
+ if err != nil {
+ return id, err
+ }
+ // 操作线上库了,保存数据,同步过来
+ // 考虑到,此时线上库可能有,可能没有,你要有一个 UPSERT 的写法
+ // INSERT or UPDATE
+ // 如果数据库有,那么就更新,不然就插入
+ err = c.readerDAO.Upsert(ctx, artn)
+ return id, err
+}
+
+func (c *CachedArticleRepository) Create(ctx context.Context, art domain.Article) (int64, error) {
+ defer func() {
+ // 清空缓存
+ c.cache.DelFirstPage(ctx, art.Author.Id)
+ }()
+ return c.dao.Insert(ctx, dao.Article{
+ Title: art.Title,
+ Content: art.Content,
+ AuthorId: art.Author.Id,
+ Status: uint8(art.Status),
+ })
+}
+
+func (c *CachedArticleRepository) Update(ctx context.Context, art domain.Article) error {
+ defer func() {
+ // 清空缓存
+ c.cache.DelFirstPage(ctx, art.Author.Id)
+ }()
+ return c.dao.UpdateById(ctx, dao.Article{
+ Id: art.Id,
+ Title: art.Title,
+ Content: art.Content,
+ AuthorId: art.Author.Id,
+ Status: uint8(art.Status),
+ })
+}
+
+func (c *CachedArticleRepository) toEntity(art domain.Article) dao.Article {
+ return dao.Article{
+ Id: art.Id,
+ Title: art.Title,
+ Content: art.Content,
+ AuthorId: art.Author.Id,
+ Status: uint8(art.Status),
+ }
+}
+
+func (c *CachedArticleRepository) preCache(ctx context.Context, data []domain.Article) {
+ if len(data) > 0 && len(data[0].Content) < 1024*1024 {
+ err := c.cache.Set(ctx, data[0])
+ if err != nil {
+ c.l.Error("提前预加载缓存失败", logger.Error(err))
+ }
+ }
+}
+
+func NewArticleRepository(dao dao.ArticleDAO,
+ c cache.ArticleCache,
+ userRepo repository.UserRepository,
+ l logger.LoggerV1) ArticleRepository {
+ return &CachedArticleRepository{
+ dao: dao,
+ cache: c,
+ l: l,
+ userRepo: userRepo,
+ }
+}
diff --git a/webook/internal/repository/article/article_author.go b/webook/internal/repository/article/article_author.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7bea44bab2001804b223cfb13941ada0da3cb3e
--- /dev/null
+++ b/webook/internal/repository/article/article_author.go
@@ -0,0 +1,11 @@
+package article
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+)
+
+type ArticleAuthorRepository interface {
+ Create(ctx context.Context, art domain.Article) (int64, error)
+ Update(ctx context.Context, art domain.Article) error
+}
diff --git a/webook/internal/repository/article/article_reader.go b/webook/internal/repository/article/article_reader.go
new file mode 100644
index 0000000000000000000000000000000000000000..43bbabb50c71d6adfab6ffcf45ea1e1c21d6897a
--- /dev/null
+++ b/webook/internal/repository/article/article_reader.go
@@ -0,0 +1,11 @@
+package article
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+)
+
+type ArticleReaderRepository interface {
+ // Save 有就更新,没有就新建,即 upsert 的语义
+ Save(ctx context.Context, art domain.Article) (int64, error)
+}
diff --git a/webook/internal/repository/article/mocks/article.mock.go b/webook/internal/repository/article/mocks/article.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..6935b54fc26d86c19000009182d305f781e5340e
--- /dev/null
+++ b/webook/internal/repository/article/mocks/article.mock.go
@@ -0,0 +1,94 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/article/article.go
+
+// Package artrepomocks is a generated GoMock package.
+package artrepomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockArticleRepository is a mock of ArticleRepository interface.
+type MockArticleRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleRepositoryMockRecorder
+}
+
+// MockArticleRepositoryMockRecorder is the mock recorder for MockArticleRepository.
+type MockArticleRepositoryMockRecorder struct {
+ mock *MockArticleRepository
+}
+
+// NewMockArticleRepository creates a new mock instance.
+func NewMockArticleRepository(ctrl *gomock.Controller) *MockArticleRepository {
+ mock := &MockArticleRepository{ctrl: ctrl}
+ mock.recorder = &MockArticleRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleRepository) EXPECT() *MockArticleRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockArticleRepository) Create(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Create", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockArticleRepositoryMockRecorder) Create(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockArticleRepository)(nil).Create), ctx, art)
+}
+
+// Sync mocks base method.
+func (m *MockArticleRepository) Sync(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Sync", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Sync indicates an expected call of Sync.
+func (mr *MockArticleRepositoryMockRecorder) Sync(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockArticleRepository)(nil).Sync), ctx, art)
+}
+
+// SyncStatus mocks base method.
+func (m *MockArticleRepository) SyncStatus(ctx context.Context, id, author int64, status domain.ArticleStatus) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SyncStatus", ctx, id, author, status)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SyncStatus indicates an expected call of SyncStatus.
+func (mr *MockArticleRepositoryMockRecorder) SyncStatus(ctx, id, author, status interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatus", reflect.TypeOf((*MockArticleRepository)(nil).SyncStatus), ctx, id, author, status)
+}
+
+// Update mocks base method.
+func (m *MockArticleRepository) Update(ctx context.Context, art domain.Article) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", ctx, art)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockArticleRepositoryMockRecorder) Update(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockArticleRepository)(nil).Update), ctx, art)
+}
diff --git a/webook/internal/repository/article/mocks/article_author.mock.go b/webook/internal/repository/article/mocks/article_author.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..ffd4cb497866c9e2f932491227d5fa7eff0f11b0
--- /dev/null
+++ b/webook/internal/repository/article/mocks/article_author.mock.go
@@ -0,0 +1,65 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/article/article_author.go
+
+// Package artrepomocks is a generated GoMock package.
+package artrepomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockArticleAuthorRepository is a mock of ArticleAuthorRepository interface.
+type MockArticleAuthorRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleAuthorRepositoryMockRecorder
+}
+
+// MockArticleAuthorRepositoryMockRecorder is the mock recorder for MockArticleAuthorRepository.
+type MockArticleAuthorRepositoryMockRecorder struct {
+ mock *MockArticleAuthorRepository
+}
+
+// NewMockArticleAuthorRepository creates a new mock instance.
+func NewMockArticleAuthorRepository(ctrl *gomock.Controller) *MockArticleAuthorRepository {
+ mock := &MockArticleAuthorRepository{ctrl: ctrl}
+ mock.recorder = &MockArticleAuthorRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleAuthorRepository) EXPECT() *MockArticleAuthorRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockArticleAuthorRepository) Create(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Create", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockArticleAuthorRepositoryMockRecorder) Create(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockArticleAuthorRepository)(nil).Create), ctx, art)
+}
+
+// Update mocks base method.
+func (m *MockArticleAuthorRepository) Update(ctx context.Context, art domain.Article) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", ctx, art)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockArticleAuthorRepositoryMockRecorder) Update(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockArticleAuthorRepository)(nil).Update), ctx, art)
+}
diff --git a/webook/internal/repository/article/mocks/article_reader.mock.go b/webook/internal/repository/article/mocks/article_reader.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..1abcec0f37964f7ca387d6cd29e69eba68c4d960
--- /dev/null
+++ b/webook/internal/repository/article/mocks/article_reader.mock.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/article/article_reader.go
+
+// Package artrepomocks is a generated GoMock package.
+package artrepomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockArticleReaderRepository is a mock of ArticleReaderRepository interface.
+type MockArticleReaderRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleReaderRepositoryMockRecorder
+}
+
+// MockArticleReaderRepositoryMockRecorder is the mock recorder for MockArticleReaderRepository.
+type MockArticleReaderRepositoryMockRecorder struct {
+ mock *MockArticleReaderRepository
+}
+
+// NewMockArticleReaderRepository creates a new mock instance.
+func NewMockArticleReaderRepository(ctrl *gomock.Controller) *MockArticleReaderRepository {
+ mock := &MockArticleReaderRepository{ctrl: ctrl}
+ mock.recorder = &MockArticleReaderRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleReaderRepository) EXPECT() *MockArticleReaderRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Save mocks base method.
+func (m *MockArticleReaderRepository) Save(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Save indicates an expected call of Save.
+func (mr *MockArticleReaderRepositoryMockRecorder) Save(ctx, art interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockArticleReaderRepository)(nil).Save), ctx, art)
+}
diff --git a/webook/internal/repository/cache/article.go b/webook/internal/repository/cache/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..bfa251a564236963449357d6e479b7025800756b
--- /dev/null
+++ b/webook/internal/repository/cache/article.go
@@ -0,0 +1,122 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+type ArticleCache interface {
+ // GetFirstPage 只缓存第第一页的数据
+ // 并且不缓存整个 Content
+ GetFirstPage(ctx context.Context, author int64) ([]domain.Article, error)
+ SetFirstPage(ctx context.Context, author int64, arts []domain.Article) error
+ DelFirstPage(ctx context.Context, author int64) error
+
+ Set(ctx context.Context, art domain.Article) error
+ Get(ctx context.Context, id int64) (domain.Article, error)
+
+ // SetPub 正常来说,创作者和读者的 Redis 集群要分开,因为读者是一个核心中的核心
+ SetPub(ctx context.Context, article domain.Article) error
+ DelPub(ctx context.Context, id int64) error
+ GetPub(ctx context.Context, id int64) (domain.Article, error)
+}
+
+type RedisArticleCache struct {
+ client redis.Cmdable
+}
+
+func NewRedisArticleCache(client redis.Cmdable) ArticleCache {
+ return &RedisArticleCache{
+ client: client,
+ }
+}
+
+func (r *RedisArticleCache) GetPub(ctx context.Context, id int64) (domain.Article, error) {
+ // 可以直接使用 Bytes 方法来获得 []byte
+ data, err := r.client.Get(ctx, r.readerArtKey(id)).Bytes()
+ if err != nil {
+ return domain.Article{}, err
+ }
+ var res domain.Article
+ err = json.Unmarshal(data, &res)
+ return res, err
+}
+
+func (r *RedisArticleCache) SetPub(ctx context.Context, art domain.Article) error {
+ data, err := json.Marshal(art)
+ if err != nil {
+ return err
+ }
+ return r.client.Set(ctx, r.readerArtKey(art.Id),
+ data,
+ // 设置长过期时间
+ time.Minute*30).Err()
+}
+
+func (r *RedisArticleCache) DelPub(ctx context.Context, id int64) error {
+ return r.client.Del(ctx, r.readerArtKey(id)).Err()
+}
+
+func (r *RedisArticleCache) Get(ctx context.Context, id int64) (domain.Article, error) {
+ // 可以直接使用 Bytes 方法来获得 []byte
+ data, err := r.client.Get(ctx, r.authorArtKey(id)).Bytes()
+ if err != nil {
+ return domain.Article{}, err
+ }
+ var res domain.Article
+ err = json.Unmarshal(data, &res)
+ return res, err
+}
+
+func (r *RedisArticleCache) Set(ctx context.Context, art domain.Article) error {
+ data, err := json.Marshal(art)
+ if err != nil {
+ return err
+ }
+ return r.client.Set(ctx, r.authorArtKey(art.Id), data, time.Minute).Err()
+}
+
+func (r *RedisArticleCache) DelFirstPage(ctx context.Context, author int64) error {
+ return r.client.Del(ctx, r.firstPageKey(author)).Err()
+}
+
+func (r *RedisArticleCache) GetFirstPage(ctx context.Context, author int64) ([]domain.Article, error) {
+ bs, err := r.client.Get(ctx, r.firstPageKey(author)).Bytes()
+ if err != nil {
+ return nil, err
+ }
+ var arts []domain.Article
+ err = json.Unmarshal(bs, &arts)
+ return arts, err
+}
+
+func (r *RedisArticleCache) SetFirstPage(ctx context.Context, author int64, arts []domain.Article) error {
+ for i := range arts {
+ // 只缓存摘要部分
+ arts[i].Content = arts[i].Abstract()
+ }
+ bs, err := json.Marshal(arts)
+ if err != nil {
+ return err
+ }
+ return r.client.Set(ctx, r.firstPageKey(author),
+ bs, time.Minute*10).Err()
+}
+
+// 创作端的缓存设置
+func (r *RedisArticleCache) authorArtKey(id int64) string {
+ return fmt.Sprintf("article:author:%d", id)
+}
+
+// 读者端的缓存设置
+func (r *RedisArticleCache) readerArtKey(id int64) string {
+ return fmt.Sprintf("article:reader:%d", id)
+}
+
+func (r *RedisArticleCache) firstPageKey(author int64) string {
+ return fmt.Sprintf("article:first_page:%d", author)
+}
diff --git a/webook/internal/repository/cache/code.go b/webook/internal/repository/cache/code.go
new file mode 100644
index 0000000000000000000000000000000000000000..98f23c60edfd0065fe215849c5f9651574439504
--- /dev/null
+++ b/webook/internal/repository/cache/code.go
@@ -0,0 +1,104 @@
+package cache
+
+import (
+ "context"
+ _ "embed"
+ "errors"
+ "fmt"
+ "github.com/redis/go-redis/v9"
+ "go.uber.org/zap"
+)
+
+var (
+ ErrCodeSendTooMany = errors.New("发送验证码太频繁")
+ ErrCodeVerifyTooManyTimes = errors.New("验证次数太多")
+ ErrUnknownForCode = errors.New("我也不知发生什么了,反正是跟 code 有关")
+)
+
+// 编译器会在编译的时候,把 set_code 的代码放进来这个 luaSetCode 变量里
+//
+//go:embed lua/set_code.lua
+var luaSetCode string
+
+//go:embed lua/verify_code.lua
+var luaVerifyCode string
+
+type CodeCache interface {
+ Set(ctx context.Context, biz, phone, code string) error
+ Verify(ctx context.Context, biz, phone, inputCode string) (bool, error)
+}
+
+type RedisCodeCache struct {
+ client redis.Cmdable
+}
+
+// NewCodeCacheGoBestPractice Go 的最佳实践是返回具体类型
+func NewCodeCacheGoBestPractice(client redis.Cmdable) *RedisCodeCache {
+ return &RedisCodeCache{
+ client: client,
+ }
+}
+
+func NewCodeCache(client redis.Cmdable) CodeCache {
+ return &RedisCodeCache{
+ client: client,
+ }
+}
+
+func (c *RedisCodeCache) Set(ctx context.Context, biz, phone, code string) error {
+ res, err := c.client.Eval(ctx, luaSetCode, []string{c.key(biz, phone)}, code).Int()
+ if err != nil {
+ return err
+ }
+ switch res {
+ case 0:
+ // 毫无问题
+ return nil
+ case -1:
+ // 发送太频繁
+ zap.L().Warn("短信发送太频繁",
+ zap.String("biz", biz),
+ // phone 是不能直接记
+ zap.String("phone", phone))
+ // 你要在对应的告警系统里面配置,
+ // 比如说规则,一分钟内出现超过100次 WARN,你就告警
+ return ErrCodeSendTooMany
+ //case -2:
+ // return
+ default:
+ // 系统错误
+ return errors.New("系统错误")
+ }
+}
+
+func (c *RedisCodeCache) Verify(ctx context.Context, biz, phone, inputCode string) (bool, error) {
+ res, err := c.client.Eval(ctx, luaVerifyCode, []string{c.key(biz, phone)}, inputCode).Int()
+ if err != nil {
+ return false, err
+ }
+ switch res {
+ case 0:
+ return true, nil
+ case -1:
+ // 正常来说,如果频繁出现这个错误,你就要告警,因为有人搞你
+ return false, ErrCodeVerifyTooManyTimes
+ case -2:
+ return false, nil
+ //default:
+ // return false, ErrUnknownForCode
+ }
+ return false, ErrUnknownForCode
+}
+
+//func (c *RedisCodeCache) Verify(ctx context.Context, biz, phone, code string) error {
+//
+//}
+
+func (c *RedisCodeCache) key(biz, phone string) string {
+ return fmt.Sprintf("phone_code:%s:%s", biz, phone)
+}
+
+// LocalCodeCache 假如说你要切换这个,你是不是得把 lua 脚本的逻辑,在这里再写一遍?
+type LocalCodeCache struct {
+ client redis.Cmdable
+}
diff --git a/webook/internal/repository/cache/code_test.go b/webook/internal/repository/cache/code_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..881bd759d032f49f846c34fcaa60a1b5baee0b6a
--- /dev/null
+++ b/webook/internal/repository/cache/code_test.go
@@ -0,0 +1,117 @@
+package cache
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache/redismocks"
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "testing"
+)
+
+func TestRedisCodeCache_Set(t *testing.T) {
+ testCases := []struct {
+ name string
+ mock func(ctrl *gomock.Controller) redis.Cmdable
+ // 输入
+ ctx context.Context
+ biz string
+ phone string
+ code string
+ // 输出
+ wantErr error
+ }{
+ {
+ name: "验证码设置成功",
+ mock: func(ctrl *gomock.Controller) redis.Cmdable {
+ cmd := redismocks.NewMockCmdable(ctrl)
+ res := redis.NewCmd(context.Background())
+ //res.SetErr(nil)
+ res.SetVal(int64(0))
+ cmd.EXPECT().Eval(gomock.Any(), luaSetCode,
+ []string{"phone_code:login:152"},
+ []any{"123456"},
+ ).Return(res)
+ return cmd
+ },
+ ctx: context.Background(),
+ biz: "login",
+ phone: "152",
+ code: "123456",
+ wantErr: nil,
+ },
+ {
+ name: "redis错误",
+ mock: func(ctrl *gomock.Controller) redis.Cmdable {
+ cmd := redismocks.NewMockCmdable(ctrl)
+ res := redis.NewCmd(context.Background())
+ res.SetErr(errors.New("mock redis 错误"))
+ //res.SetVal(int64(0))
+ cmd.EXPECT().Eval(gomock.Any(), luaSetCode,
+ []string{"phone_code:login:152"},
+ []any{"123456"},
+ ).Return(res)
+ return cmd
+ },
+
+ ctx: context.Background(),
+ biz: "login",
+ phone: "152",
+ code: "123456",
+
+ wantErr: errors.New("mock redis 错误"),
+ },
+ {
+ name: "发送太频繁",
+ mock: func(ctrl *gomock.Controller) redis.Cmdable {
+ cmd := redismocks.NewMockCmdable(ctrl)
+ res := redis.NewCmd(context.Background())
+ //res.SetErr(nil)
+ res.SetVal(int64(-1))
+ cmd.EXPECT().Eval(gomock.Any(), luaSetCode,
+ []string{"phone_code:login:152"},
+ []any{"123456"},
+ ).Return(res)
+ return cmd
+ },
+
+ ctx: context.Background(),
+ biz: "login",
+ phone: "152",
+ code: "123456",
+
+ wantErr: ErrCodeSendTooMany,
+ },
+ {
+ name: "系统错误",
+ mock: func(ctrl *gomock.Controller) redis.Cmdable {
+ cmd := redismocks.NewMockCmdable(ctrl)
+ res := redis.NewCmd(context.Background())
+ //res.SetErr(nil)
+ res.SetVal(int64(-10))
+ cmd.EXPECT().Eval(gomock.Any(), luaSetCode,
+ []string{"phone_code:login:152"},
+ []any{"123456"},
+ ).Return(res)
+ return cmd
+ },
+
+ ctx: context.Background(),
+ biz: "login",
+ phone: "152",
+ code: "123456",
+ wantErr: errors.New("系统错误"),
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ c := NewCodeCache(tc.mock(ctrl))
+ err := c.Set(tc.ctx, tc.biz, tc.phone, tc.code)
+ assert.Equal(t, tc.wantErr, err)
+ })
+ }
+}
diff --git a/webook/internal/repository/cache/local_ranking.go b/webook/internal/repository/cache/local_ranking.go
new file mode 100644
index 0000000000000000000000000000000000000000..41763a19f92a4c889d69ff93e4d4985a362f071b
--- /dev/null
+++ b/webook/internal/repository/cache/local_ranking.go
@@ -0,0 +1,56 @@
+package cache
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "time"
+)
+
+type RankingLocalCache struct {
+ // 我用我的泛型封装
+ // 你可以考虑直接使用 uber 的,或者 SDK 自带的
+ topN *atomicx.Value[[]domain.Article]
+ ddl *atomicx.Value[time.Time]
+ expiration time.Duration
+}
+
+func NewRankingLocalCache() *RankingLocalCache {
+ return &RankingLocalCache{
+ topN: atomicx.NewValue[[]domain.Article](),
+ ddl: atomicx.NewValueOf(time.Now()),
+ // 永不过期,或者非常长,或者对齐到 redis 的过期时间,都行
+ expiration: time.Minute * 10,
+ }
+}
+
+//func (r *RankingLocalCache) Preload(ctx context.Context) {
+//
+//}
+
+func (r *RankingLocalCache) Set(ctx context.Context, arts []domain.Article) error {
+ // 也可以按照 id => Article 缓存
+ r.topN.Store(arts)
+ ddl := time.Now().Add(r.expiration)
+ r.ddl.Store(ddl)
+ return nil
+}
+
+func (r *RankingLocalCache) Get(ctx context.Context) ([]domain.Article, error) {
+ ddl := r.ddl.Load()
+ arts := r.topN.Load()
+ if len(arts) == 0 || ddl.Before(time.Now()) {
+ return nil, errors.New("本地缓存未命中")
+ }
+ return arts, nil
+}
+func (r *RankingLocalCache) ForceGet(ctx context.Context) ([]domain.Article, error) {
+ arts := r.topN.Load()
+ return arts, nil
+}
+
+type item struct {
+ arts []domain.Article
+ ddl time.Time
+}
diff --git a/webook/internal/repository/cache/lua/set_code.lua b/webook/internal/repository/cache/lua/set_code.lua
new file mode 100644
index 0000000000000000000000000000000000000000..8c1c70616279e9b39dfc46880b6b92c5729784c1
--- /dev/null
+++ b/webook/internal/repository/cache/lua/set_code.lua
@@ -0,0 +1,27 @@
+--你的验证码在 Redis 上的 key
+-- phone_code:login:152xxxxxxxx
+local key = KEYS[1]
+-- 验证次数,我们一个验证码,最多重复三次,这个记录还可以验证几次
+-- phone_code:login:152xxxxxxxx:cnt
+local cntKey = key..":cnt"
+-- 你的验证码 123456
+local val= ARGV[1]
+-- 过期时间
+local ttl = tonumber(redis.call("ttl", key))
+if ttl == -1 then
+ -- key 存在,但是没有过期时间
+ -- 系统错误,你的同事手贱,手动设置了这个 key,但是没给过期时间
+ return -2
+ -- 540 = 600-60 九分钟
+elseif ttl == -2 or ttl < 540 then
+ redis.call("set", key, val)
+ redis.call("expire", key, 600)
+ redis.call("set", cntKey, 3)
+ redis.call("expire", cntKey, 600)
+ -- 完美,符合预期
+ return 0
+else
+ -- 发送太频繁
+ return -1
+end
+
diff --git a/webook/internal/repository/cache/lua/verify_code.lua b/webook/internal/repository/cache/lua/verify_code.lua
new file mode 100644
index 0000000000000000000000000000000000000000..4fc42ca73ab56dc3dc2f9874f453eb6e1ce4047e
--- /dev/null
+++ b/webook/internal/repository/cache/lua/verify_code.lua
@@ -0,0 +1,22 @@
+local key = KEYS[1]
+-- 用户输入的 code
+local expectedCode = ARGV[1]
+local code = redis.call("get", key)
+local cntKey = key..":cnt"
+-- 转成一个数字
+local cnt = tonumber(redis.call("get", cntKey))
+if cnt <= 0 then
+-- 说明,用户一直输错,有人搞你
+-- 或者已经用过了,也是有人搞你
+ return -1
+elseif expectedCode == code then
+ -- 输入对了
+ -- 用完,不能再用了
+ redis.call("set", cntKey, -1)
+ return 0
+else
+ -- 用户手一抖,输错了
+ -- 可验证次数 -1
+ redis.call("decr", cntKey)
+ return -2
+end
\ No newline at end of file
diff --git a/webook/internal/repository/cache/mocks/user.mock.go b/webook/internal/repository/cache/mocks/user.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..301010fcd81dbf65fb1b00a26dc9d076b6c6ead6
--- /dev/null
+++ b/webook/internal/repository/cache/mocks/user.mock.go
@@ -0,0 +1,65 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/cache/user.go
+
+// Package cachemocks is a generated GoMock package.
+package cachemocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockUserCache is a mock of UserCache interface.
+type MockUserCache struct {
+ ctrl *gomock.Controller
+ recorder *MockUserCacheMockRecorder
+}
+
+// MockUserCacheMockRecorder is the mock recorder for MockUserCache.
+type MockUserCacheMockRecorder struct {
+ mock *MockUserCache
+}
+
+// NewMockUserCache creates a new mock instance.
+func NewMockUserCache(ctrl *gomock.Controller) *MockUserCache {
+ mock := &MockUserCache{ctrl: ctrl}
+ mock.recorder = &MockUserCacheMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUserCache) EXPECT() *MockUserCacheMockRecorder {
+ return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockUserCache) Get(ctx context.Context, id int64) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", ctx, id)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockUserCacheMockRecorder) Get(ctx, id interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUserCache)(nil).Get), ctx, id)
+}
+
+// Set mocks base method.
+func (m *MockUserCache) Set(ctx context.Context, u domain.User) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Set", ctx, u)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Set indicates an expected call of Set.
+func (mr *MockUserCacheMockRecorder) Set(ctx, u interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockUserCache)(nil).Set), ctx, u)
+}
diff --git a/webook/internal/repository/cache/ranking.go b/webook/internal/repository/cache/ranking.go
new file mode 100644
index 0000000000000000000000000000000000000000..724f88255f4c0e50dd213e1f401388b4dc75e3ff
--- /dev/null
+++ b/webook/internal/repository/cache/ranking.go
@@ -0,0 +1,52 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+type RankingCache interface {
+ Set(ctx context.Context, arts []domain.Article) error
+ Get(ctx context.Context) ([]domain.Article, error)
+}
+
+type RankingRedisCache struct {
+ client redis.Cmdable
+ key string
+}
+
+func NewRankingRedisCache(client redis.Cmdable) *RankingRedisCache {
+ return &RankingRedisCache{
+ client: client,
+ key: "ranking",
+ }
+
+}
+
+func (r *RankingRedisCache) Set(ctx context.Context, arts []domain.Article) error {
+ // 你可以趁机,把 article 写到缓存里面 id => article
+ for i := 0; i < len(arts); i++ {
+ arts[i].Content = ""
+ }
+ val, err := json.Marshal(arts)
+ if err != nil {
+ return err
+ }
+ // 这个过期时间要稍微长一点,最好是超过计算热榜的时间(包含重试在内的时间)
+ // 你甚至可以直接永不过期
+ return r.client.Set(ctx, r.key, val, time.Minute*10).Err()
+}
+
+func (r *RankingRedisCache) Get(ctx context.Context) ([]domain.Article, error) {
+ data, err := r.client.Get(ctx, r.key).Bytes()
+ if err != nil {
+ return nil, err
+ }
+
+ var res []domain.Article
+ err = json.Unmarshal(data, &res)
+ return res, err
+}
diff --git a/webook/internal/repository/cache/redismocks/cmdable.mock.go b/webook/internal/repository/cache/redismocks/cmdable.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..af4738b21a3d817588407550e4a37a6bc7aaf379
--- /dev/null
+++ b/webook/internal/repository/cache/redismocks/cmdable.mock.go
@@ -0,0 +1,5035 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/redis/go-redis/v9 (interfaces: Cmdable)
+
+// Package redismocks is a generated GoMock package.
+package redismocks
+
+import (
+ context "context"
+ reflect "reflect"
+ time "time"
+
+ redis "github.com/redis/go-redis/v9"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockCmdable is a mock of Cmdable interface.
+type MockCmdable struct {
+ ctrl *gomock.Controller
+ recorder *MockCmdableMockRecorder
+}
+
+// MockCmdableMockRecorder is the mock recorder for MockCmdable.
+type MockCmdableMockRecorder struct {
+ mock *MockCmdable
+}
+
+// NewMockCmdable creates a new mock instance.
+func NewMockCmdable(ctrl *gomock.Controller) *MockCmdable {
+ mock := &MockCmdable{ctrl: ctrl}
+ mock.recorder = &MockCmdableMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCmdable) EXPECT() *MockCmdableMockRecorder {
+ return m.recorder
+}
+
+// ACLDryRun mocks base method.
+func (m *MockCmdable) ACLDryRun(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ACLDryRun", varargs...)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ACLDryRun indicates an expected call of ACLDryRun.
+func (mr *MockCmdableMockRecorder) ACLDryRun(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLDryRun", reflect.TypeOf((*MockCmdable)(nil).ACLDryRun), varargs...)
+}
+
+// ACLLog mocks base method.
+func (m *MockCmdable) ACLLog(arg0 context.Context, arg1 int64) *redis.ACLLogCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ACLLog", arg0, arg1)
+ ret0, _ := ret[0].(*redis.ACLLogCmd)
+ return ret0
+}
+
+// ACLLog indicates an expected call of ACLLog.
+func (mr *MockCmdableMockRecorder) ACLLog(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLLog", reflect.TypeOf((*MockCmdable)(nil).ACLLog), arg0, arg1)
+}
+
+// ACLLogReset mocks base method.
+func (m *MockCmdable) ACLLogReset(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ACLLogReset", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ACLLogReset indicates an expected call of ACLLogReset.
+func (mr *MockCmdableMockRecorder) ACLLogReset(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLLogReset", reflect.TypeOf((*MockCmdable)(nil).ACLLogReset), arg0)
+}
+
+// Append mocks base method.
+func (m *MockCmdable) Append(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Append", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Append indicates an expected call of Append.
+func (mr *MockCmdableMockRecorder) Append(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Append", reflect.TypeOf((*MockCmdable)(nil).Append), arg0, arg1, arg2)
+}
+
+// BLMPop mocks base method.
+func (m *MockCmdable) BLMPop(arg0 context.Context, arg1 time.Duration, arg2 string, arg3 int64, arg4 ...string) *redis.KeyValuesCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2, arg3}
+ for _, a := range arg4 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BLMPop", varargs...)
+ ret0, _ := ret[0].(*redis.KeyValuesCmd)
+ return ret0
+}
+
+// BLMPop indicates an expected call of BLMPop.
+func (mr *MockCmdableMockRecorder) BLMPop(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLMPop", reflect.TypeOf((*MockCmdable)(nil).BLMPop), varargs...)
+}
+
+// BLMove mocks base method.
+func (m *MockCmdable) BLMove(arg0 context.Context, arg1, arg2, arg3, arg4 string, arg5 time.Duration) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BLMove", arg0, arg1, arg2, arg3, arg4, arg5)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// BLMove indicates an expected call of BLMove.
+func (mr *MockCmdableMockRecorder) BLMove(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLMove", reflect.TypeOf((*MockCmdable)(nil).BLMove), arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+// BLPop mocks base method.
+func (m *MockCmdable) BLPop(arg0 context.Context, arg1 time.Duration, arg2 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BLPop", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// BLPop indicates an expected call of BLPop.
+func (mr *MockCmdableMockRecorder) BLPop(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLPop", reflect.TypeOf((*MockCmdable)(nil).BLPop), varargs...)
+}
+
+// BRPop mocks base method.
+func (m *MockCmdable) BRPop(arg0 context.Context, arg1 time.Duration, arg2 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BRPop", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// BRPop indicates an expected call of BRPop.
+func (mr *MockCmdableMockRecorder) BRPop(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BRPop", reflect.TypeOf((*MockCmdable)(nil).BRPop), varargs...)
+}
+
+// BRPopLPush mocks base method.
+func (m *MockCmdable) BRPopLPush(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BRPopLPush", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// BRPopLPush indicates an expected call of BRPopLPush.
+func (mr *MockCmdableMockRecorder) BRPopLPush(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BRPopLPush", reflect.TypeOf((*MockCmdable)(nil).BRPopLPush), arg0, arg1, arg2, arg3)
+}
+
+// BZMPop mocks base method.
+func (m *MockCmdable) BZMPop(arg0 context.Context, arg1 time.Duration, arg2 string, arg3 int64, arg4 ...string) *redis.ZSliceWithKeyCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2, arg3}
+ for _, a := range arg4 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BZMPop", varargs...)
+ ret0, _ := ret[0].(*redis.ZSliceWithKeyCmd)
+ return ret0
+}
+
+// BZMPop indicates an expected call of BZMPop.
+func (mr *MockCmdableMockRecorder) BZMPop(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZMPop", reflect.TypeOf((*MockCmdable)(nil).BZMPop), varargs...)
+}
+
+// BZPopMax mocks base method.
+func (m *MockCmdable) BZPopMax(arg0 context.Context, arg1 time.Duration, arg2 ...string) *redis.ZWithKeyCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BZPopMax", varargs...)
+ ret0, _ := ret[0].(*redis.ZWithKeyCmd)
+ return ret0
+}
+
+// BZPopMax indicates an expected call of BZPopMax.
+func (mr *MockCmdableMockRecorder) BZPopMax(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZPopMax", reflect.TypeOf((*MockCmdable)(nil).BZPopMax), varargs...)
+}
+
+// BZPopMin mocks base method.
+func (m *MockCmdable) BZPopMin(arg0 context.Context, arg1 time.Duration, arg2 ...string) *redis.ZWithKeyCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BZPopMin", varargs...)
+ ret0, _ := ret[0].(*redis.ZWithKeyCmd)
+ return ret0
+}
+
+// BZPopMin indicates an expected call of BZPopMin.
+func (mr *MockCmdableMockRecorder) BZPopMin(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZPopMin", reflect.TypeOf((*MockCmdable)(nil).BZPopMin), varargs...)
+}
+
+// BgRewriteAOF mocks base method.
+func (m *MockCmdable) BgRewriteAOF(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BgRewriteAOF", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// BgRewriteAOF indicates an expected call of BgRewriteAOF.
+func (mr *MockCmdableMockRecorder) BgRewriteAOF(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BgRewriteAOF", reflect.TypeOf((*MockCmdable)(nil).BgRewriteAOF), arg0)
+}
+
+// BgSave mocks base method.
+func (m *MockCmdable) BgSave(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BgSave", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// BgSave indicates an expected call of BgSave.
+func (mr *MockCmdableMockRecorder) BgSave(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BgSave", reflect.TypeOf((*MockCmdable)(nil).BgSave), arg0)
+}
+
+// BitCount mocks base method.
+func (m *MockCmdable) BitCount(arg0 context.Context, arg1 string, arg2 *redis.BitCount) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BitCount", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitCount indicates an expected call of BitCount.
+func (mr *MockCmdableMockRecorder) BitCount(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitCount", reflect.TypeOf((*MockCmdable)(nil).BitCount), arg0, arg1, arg2)
+}
+
+// BitField mocks base method.
+func (m *MockCmdable) BitField(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BitField", varargs...)
+ ret0, _ := ret[0].(*redis.IntSliceCmd)
+ return ret0
+}
+
+// BitField indicates an expected call of BitField.
+func (mr *MockCmdableMockRecorder) BitField(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitField", reflect.TypeOf((*MockCmdable)(nil).BitField), varargs...)
+}
+
+// BitOpAnd mocks base method.
+func (m *MockCmdable) BitOpAnd(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BitOpAnd", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitOpAnd indicates an expected call of BitOpAnd.
+func (mr *MockCmdableMockRecorder) BitOpAnd(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpAnd", reflect.TypeOf((*MockCmdable)(nil).BitOpAnd), varargs...)
+}
+
+// BitOpNot mocks base method.
+func (m *MockCmdable) BitOpNot(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BitOpNot", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitOpNot indicates an expected call of BitOpNot.
+func (mr *MockCmdableMockRecorder) BitOpNot(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpNot", reflect.TypeOf((*MockCmdable)(nil).BitOpNot), arg0, arg1, arg2)
+}
+
+// BitOpOr mocks base method.
+func (m *MockCmdable) BitOpOr(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BitOpOr", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitOpOr indicates an expected call of BitOpOr.
+func (mr *MockCmdableMockRecorder) BitOpOr(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpOr", reflect.TypeOf((*MockCmdable)(nil).BitOpOr), varargs...)
+}
+
+// BitOpXor mocks base method.
+func (m *MockCmdable) BitOpXor(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BitOpXor", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitOpXor indicates an expected call of BitOpXor.
+func (mr *MockCmdableMockRecorder) BitOpXor(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpXor", reflect.TypeOf((*MockCmdable)(nil).BitOpXor), varargs...)
+}
+
+// BitPos mocks base method.
+func (m *MockCmdable) BitPos(arg0 context.Context, arg1 string, arg2 int64, arg3 ...int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "BitPos", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitPos indicates an expected call of BitPos.
+func (mr *MockCmdableMockRecorder) BitPos(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitPos", reflect.TypeOf((*MockCmdable)(nil).BitPos), varargs...)
+}
+
+// BitPosSpan mocks base method.
+func (m *MockCmdable) BitPosSpan(arg0 context.Context, arg1 string, arg2 int8, arg3, arg4 int64, arg5 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BitPosSpan", arg0, arg1, arg2, arg3, arg4, arg5)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// BitPosSpan indicates an expected call of BitPosSpan.
+func (mr *MockCmdableMockRecorder) BitPosSpan(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitPosSpan", reflect.TypeOf((*MockCmdable)(nil).BitPosSpan), arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+// ClientGetName mocks base method.
+func (m *MockCmdable) ClientGetName(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientGetName", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ClientGetName indicates an expected call of ClientGetName.
+func (mr *MockCmdableMockRecorder) ClientGetName(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientGetName", reflect.TypeOf((*MockCmdable)(nil).ClientGetName), arg0)
+}
+
+// ClientID mocks base method.
+func (m *MockCmdable) ClientID(arg0 context.Context) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientID", arg0)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClientID indicates an expected call of ClientID.
+func (mr *MockCmdableMockRecorder) ClientID(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockCmdable)(nil).ClientID), arg0)
+}
+
+// ClientInfo mocks base method.
+func (m *MockCmdable) ClientInfo(arg0 context.Context) *redis.ClientInfoCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientInfo", arg0)
+ ret0, _ := ret[0].(*redis.ClientInfoCmd)
+ return ret0
+}
+
+// ClientInfo indicates an expected call of ClientInfo.
+func (mr *MockCmdableMockRecorder) ClientInfo(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientInfo", reflect.TypeOf((*MockCmdable)(nil).ClientInfo), arg0)
+}
+
+// ClientKill mocks base method.
+func (m *MockCmdable) ClientKill(arg0 context.Context, arg1 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientKill", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClientKill indicates an expected call of ClientKill.
+func (mr *MockCmdableMockRecorder) ClientKill(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientKill", reflect.TypeOf((*MockCmdable)(nil).ClientKill), arg0, arg1)
+}
+
+// ClientKillByFilter mocks base method.
+func (m *MockCmdable) ClientKillByFilter(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ClientKillByFilter", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClientKillByFilter indicates an expected call of ClientKillByFilter.
+func (mr *MockCmdableMockRecorder) ClientKillByFilter(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientKillByFilter", reflect.TypeOf((*MockCmdable)(nil).ClientKillByFilter), varargs...)
+}
+
+// ClientList mocks base method.
+func (m *MockCmdable) ClientList(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientList", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ClientList indicates an expected call of ClientList.
+func (mr *MockCmdableMockRecorder) ClientList(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientList", reflect.TypeOf((*MockCmdable)(nil).ClientList), arg0)
+}
+
+// ClientPause mocks base method.
+func (m *MockCmdable) ClientPause(arg0 context.Context, arg1 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientPause", arg0, arg1)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ClientPause indicates an expected call of ClientPause.
+func (mr *MockCmdableMockRecorder) ClientPause(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientPause", reflect.TypeOf((*MockCmdable)(nil).ClientPause), arg0, arg1)
+}
+
+// ClientUnblock mocks base method.
+func (m *MockCmdable) ClientUnblock(arg0 context.Context, arg1 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientUnblock", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClientUnblock indicates an expected call of ClientUnblock.
+func (mr *MockCmdableMockRecorder) ClientUnblock(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnblock", reflect.TypeOf((*MockCmdable)(nil).ClientUnblock), arg0, arg1)
+}
+
+// ClientUnblockWithError mocks base method.
+func (m *MockCmdable) ClientUnblockWithError(arg0 context.Context, arg1 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientUnblockWithError", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClientUnblockWithError indicates an expected call of ClientUnblockWithError.
+func (mr *MockCmdableMockRecorder) ClientUnblockWithError(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnblockWithError", reflect.TypeOf((*MockCmdable)(nil).ClientUnblockWithError), arg0, arg1)
+}
+
+// ClientUnpause mocks base method.
+func (m *MockCmdable) ClientUnpause(arg0 context.Context) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClientUnpause", arg0)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ClientUnpause indicates an expected call of ClientUnpause.
+func (mr *MockCmdableMockRecorder) ClientUnpause(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnpause", reflect.TypeOf((*MockCmdable)(nil).ClientUnpause), arg0)
+}
+
+// ClusterAddSlots mocks base method.
+func (m *MockCmdable) ClusterAddSlots(arg0 context.Context, arg1 ...int) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ClusterAddSlots", varargs...)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterAddSlots indicates an expected call of ClusterAddSlots.
+func (mr *MockCmdableMockRecorder) ClusterAddSlots(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterAddSlots", reflect.TypeOf((*MockCmdable)(nil).ClusterAddSlots), varargs...)
+}
+
+// ClusterAddSlotsRange mocks base method.
+func (m *MockCmdable) ClusterAddSlotsRange(arg0 context.Context, arg1, arg2 int) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterAddSlotsRange", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterAddSlotsRange indicates an expected call of ClusterAddSlotsRange.
+func (mr *MockCmdableMockRecorder) ClusterAddSlotsRange(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterAddSlotsRange", reflect.TypeOf((*MockCmdable)(nil).ClusterAddSlotsRange), arg0, arg1, arg2)
+}
+
+// ClusterCountFailureReports mocks base method.
+func (m *MockCmdable) ClusterCountFailureReports(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterCountFailureReports", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClusterCountFailureReports indicates an expected call of ClusterCountFailureReports.
+func (mr *MockCmdableMockRecorder) ClusterCountFailureReports(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterCountFailureReports", reflect.TypeOf((*MockCmdable)(nil).ClusterCountFailureReports), arg0, arg1)
+}
+
+// ClusterCountKeysInSlot mocks base method.
+func (m *MockCmdable) ClusterCountKeysInSlot(arg0 context.Context, arg1 int) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterCountKeysInSlot", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClusterCountKeysInSlot indicates an expected call of ClusterCountKeysInSlot.
+func (mr *MockCmdableMockRecorder) ClusterCountKeysInSlot(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterCountKeysInSlot", reflect.TypeOf((*MockCmdable)(nil).ClusterCountKeysInSlot), arg0, arg1)
+}
+
+// ClusterDelSlots mocks base method.
+func (m *MockCmdable) ClusterDelSlots(arg0 context.Context, arg1 ...int) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ClusterDelSlots", varargs...)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterDelSlots indicates an expected call of ClusterDelSlots.
+func (mr *MockCmdableMockRecorder) ClusterDelSlots(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterDelSlots", reflect.TypeOf((*MockCmdable)(nil).ClusterDelSlots), varargs...)
+}
+
+// ClusterDelSlotsRange mocks base method.
+func (m *MockCmdable) ClusterDelSlotsRange(arg0 context.Context, arg1, arg2 int) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterDelSlotsRange", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterDelSlotsRange indicates an expected call of ClusterDelSlotsRange.
+func (mr *MockCmdableMockRecorder) ClusterDelSlotsRange(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterDelSlotsRange", reflect.TypeOf((*MockCmdable)(nil).ClusterDelSlotsRange), arg0, arg1, arg2)
+}
+
+// ClusterFailover mocks base method.
+func (m *MockCmdable) ClusterFailover(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterFailover", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterFailover indicates an expected call of ClusterFailover.
+func (mr *MockCmdableMockRecorder) ClusterFailover(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterFailover", reflect.TypeOf((*MockCmdable)(nil).ClusterFailover), arg0)
+}
+
+// ClusterForget mocks base method.
+func (m *MockCmdable) ClusterForget(arg0 context.Context, arg1 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterForget", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterForget indicates an expected call of ClusterForget.
+func (mr *MockCmdableMockRecorder) ClusterForget(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterForget", reflect.TypeOf((*MockCmdable)(nil).ClusterForget), arg0, arg1)
+}
+
+// ClusterGetKeysInSlot mocks base method.
+func (m *MockCmdable) ClusterGetKeysInSlot(arg0 context.Context, arg1, arg2 int) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterGetKeysInSlot", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ClusterGetKeysInSlot indicates an expected call of ClusterGetKeysInSlot.
+func (mr *MockCmdableMockRecorder) ClusterGetKeysInSlot(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterGetKeysInSlot", reflect.TypeOf((*MockCmdable)(nil).ClusterGetKeysInSlot), arg0, arg1, arg2)
+}
+
+// ClusterInfo mocks base method.
+func (m *MockCmdable) ClusterInfo(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterInfo", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ClusterInfo indicates an expected call of ClusterInfo.
+func (mr *MockCmdableMockRecorder) ClusterInfo(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterInfo", reflect.TypeOf((*MockCmdable)(nil).ClusterInfo), arg0)
+}
+
+// ClusterKeySlot mocks base method.
+func (m *MockCmdable) ClusterKeySlot(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterKeySlot", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ClusterKeySlot indicates an expected call of ClusterKeySlot.
+func (mr *MockCmdableMockRecorder) ClusterKeySlot(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterKeySlot", reflect.TypeOf((*MockCmdable)(nil).ClusterKeySlot), arg0, arg1)
+}
+
+// ClusterLinks mocks base method.
+func (m *MockCmdable) ClusterLinks(arg0 context.Context) *redis.ClusterLinksCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterLinks", arg0)
+ ret0, _ := ret[0].(*redis.ClusterLinksCmd)
+ return ret0
+}
+
+// ClusterLinks indicates an expected call of ClusterLinks.
+func (mr *MockCmdableMockRecorder) ClusterLinks(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterLinks", reflect.TypeOf((*MockCmdable)(nil).ClusterLinks), arg0)
+}
+
+// ClusterMeet mocks base method.
+func (m *MockCmdable) ClusterMeet(arg0 context.Context, arg1, arg2 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterMeet", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterMeet indicates an expected call of ClusterMeet.
+func (mr *MockCmdableMockRecorder) ClusterMeet(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterMeet", reflect.TypeOf((*MockCmdable)(nil).ClusterMeet), arg0, arg1, arg2)
+}
+
+// ClusterMyShardID mocks base method.
+func (m *MockCmdable) ClusterMyShardID(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterMyShardID", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ClusterMyShardID indicates an expected call of ClusterMyShardID.
+func (mr *MockCmdableMockRecorder) ClusterMyShardID(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterMyShardID", reflect.TypeOf((*MockCmdable)(nil).ClusterMyShardID), arg0)
+}
+
+// ClusterNodes mocks base method.
+func (m *MockCmdable) ClusterNodes(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterNodes", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ClusterNodes indicates an expected call of ClusterNodes.
+func (mr *MockCmdableMockRecorder) ClusterNodes(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterNodes", reflect.TypeOf((*MockCmdable)(nil).ClusterNodes), arg0)
+}
+
+// ClusterReplicate mocks base method.
+func (m *MockCmdable) ClusterReplicate(arg0 context.Context, arg1 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterReplicate", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterReplicate indicates an expected call of ClusterReplicate.
+func (mr *MockCmdableMockRecorder) ClusterReplicate(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterReplicate", reflect.TypeOf((*MockCmdable)(nil).ClusterReplicate), arg0, arg1)
+}
+
+// ClusterResetHard mocks base method.
+func (m *MockCmdable) ClusterResetHard(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterResetHard", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterResetHard indicates an expected call of ClusterResetHard.
+func (mr *MockCmdableMockRecorder) ClusterResetHard(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterResetHard", reflect.TypeOf((*MockCmdable)(nil).ClusterResetHard), arg0)
+}
+
+// ClusterResetSoft mocks base method.
+func (m *MockCmdable) ClusterResetSoft(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterResetSoft", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterResetSoft indicates an expected call of ClusterResetSoft.
+func (mr *MockCmdableMockRecorder) ClusterResetSoft(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterResetSoft", reflect.TypeOf((*MockCmdable)(nil).ClusterResetSoft), arg0)
+}
+
+// ClusterSaveConfig mocks base method.
+func (m *MockCmdable) ClusterSaveConfig(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterSaveConfig", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ClusterSaveConfig indicates an expected call of ClusterSaveConfig.
+func (mr *MockCmdableMockRecorder) ClusterSaveConfig(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSaveConfig", reflect.TypeOf((*MockCmdable)(nil).ClusterSaveConfig), arg0)
+}
+
+// ClusterShards mocks base method.
+func (m *MockCmdable) ClusterShards(arg0 context.Context) *redis.ClusterShardsCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterShards", arg0)
+ ret0, _ := ret[0].(*redis.ClusterShardsCmd)
+ return ret0
+}
+
+// ClusterShards indicates an expected call of ClusterShards.
+func (mr *MockCmdableMockRecorder) ClusterShards(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterShards", reflect.TypeOf((*MockCmdable)(nil).ClusterShards), arg0)
+}
+
+// ClusterSlaves mocks base method.
+func (m *MockCmdable) ClusterSlaves(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterSlaves", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ClusterSlaves indicates an expected call of ClusterSlaves.
+func (mr *MockCmdableMockRecorder) ClusterSlaves(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSlaves", reflect.TypeOf((*MockCmdable)(nil).ClusterSlaves), arg0, arg1)
+}
+
+// ClusterSlots mocks base method.
+func (m *MockCmdable) ClusterSlots(arg0 context.Context) *redis.ClusterSlotsCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClusterSlots", arg0)
+ ret0, _ := ret[0].(*redis.ClusterSlotsCmd)
+ return ret0
+}
+
+// ClusterSlots indicates an expected call of ClusterSlots.
+func (mr *MockCmdableMockRecorder) ClusterSlots(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSlots", reflect.TypeOf((*MockCmdable)(nil).ClusterSlots), arg0)
+}
+
+// Command mocks base method.
+func (m *MockCmdable) Command(arg0 context.Context) *redis.CommandsInfoCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Command", arg0)
+ ret0, _ := ret[0].(*redis.CommandsInfoCmd)
+ return ret0
+}
+
+// Command indicates an expected call of Command.
+func (mr *MockCmdableMockRecorder) Command(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Command", reflect.TypeOf((*MockCmdable)(nil).Command), arg0)
+}
+
+// CommandGetKeys mocks base method.
+func (m *MockCmdable) CommandGetKeys(arg0 context.Context, arg1 ...interface{}) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "CommandGetKeys", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// CommandGetKeys indicates an expected call of CommandGetKeys.
+func (mr *MockCmdableMockRecorder) CommandGetKeys(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandGetKeys", reflect.TypeOf((*MockCmdable)(nil).CommandGetKeys), varargs...)
+}
+
+// CommandGetKeysAndFlags mocks base method.
+func (m *MockCmdable) CommandGetKeysAndFlags(arg0 context.Context, arg1 ...interface{}) *redis.KeyFlagsCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "CommandGetKeysAndFlags", varargs...)
+ ret0, _ := ret[0].(*redis.KeyFlagsCmd)
+ return ret0
+}
+
+// CommandGetKeysAndFlags indicates an expected call of CommandGetKeysAndFlags.
+func (mr *MockCmdableMockRecorder) CommandGetKeysAndFlags(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandGetKeysAndFlags", reflect.TypeOf((*MockCmdable)(nil).CommandGetKeysAndFlags), varargs...)
+}
+
+// CommandList mocks base method.
+func (m *MockCmdable) CommandList(arg0 context.Context, arg1 *redis.FilterBy) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CommandList", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// CommandList indicates an expected call of CommandList.
+func (mr *MockCmdableMockRecorder) CommandList(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandList", reflect.TypeOf((*MockCmdable)(nil).CommandList), arg0, arg1)
+}
+
+// ConfigGet mocks base method.
+func (m *MockCmdable) ConfigGet(arg0 context.Context, arg1 string) *redis.MapStringStringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfigGet", arg0, arg1)
+ ret0, _ := ret[0].(*redis.MapStringStringCmd)
+ return ret0
+}
+
+// ConfigGet indicates an expected call of ConfigGet.
+func (mr *MockCmdableMockRecorder) ConfigGet(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigGet", reflect.TypeOf((*MockCmdable)(nil).ConfigGet), arg0, arg1)
+}
+
+// ConfigResetStat mocks base method.
+func (m *MockCmdable) ConfigResetStat(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfigResetStat", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ConfigResetStat indicates an expected call of ConfigResetStat.
+func (mr *MockCmdableMockRecorder) ConfigResetStat(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigResetStat", reflect.TypeOf((*MockCmdable)(nil).ConfigResetStat), arg0)
+}
+
+// ConfigRewrite mocks base method.
+func (m *MockCmdable) ConfigRewrite(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfigRewrite", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ConfigRewrite indicates an expected call of ConfigRewrite.
+func (mr *MockCmdableMockRecorder) ConfigRewrite(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigRewrite", reflect.TypeOf((*MockCmdable)(nil).ConfigRewrite), arg0)
+}
+
+// ConfigSet mocks base method.
+func (m *MockCmdable) ConfigSet(arg0 context.Context, arg1, arg2 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfigSet", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ConfigSet indicates an expected call of ConfigSet.
+func (mr *MockCmdableMockRecorder) ConfigSet(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigSet", reflect.TypeOf((*MockCmdable)(nil).ConfigSet), arg0, arg1, arg2)
+}
+
+// Copy mocks base method.
+func (m *MockCmdable) Copy(arg0 context.Context, arg1, arg2 string, arg3 int, arg4 bool) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Copy", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Copy indicates an expected call of Copy.
+func (mr *MockCmdableMockRecorder) Copy(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockCmdable)(nil).Copy), arg0, arg1, arg2, arg3, arg4)
+}
+
+// DBSize mocks base method.
+func (m *MockCmdable) DBSize(arg0 context.Context) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DBSize", arg0)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// DBSize indicates an expected call of DBSize.
+func (mr *MockCmdableMockRecorder) DBSize(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBSize", reflect.TypeOf((*MockCmdable)(nil).DBSize), arg0)
+}
+
+// DebugObject mocks base method.
+func (m *MockCmdable) DebugObject(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DebugObject", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// DebugObject indicates an expected call of DebugObject.
+func (mr *MockCmdableMockRecorder) DebugObject(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DebugObject", reflect.TypeOf((*MockCmdable)(nil).DebugObject), arg0, arg1)
+}
+
+// Decr mocks base method.
+func (m *MockCmdable) Decr(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Decr", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Decr indicates an expected call of Decr.
+func (mr *MockCmdableMockRecorder) Decr(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decr", reflect.TypeOf((*MockCmdable)(nil).Decr), arg0, arg1)
+}
+
+// DecrBy mocks base method.
+func (m *MockCmdable) DecrBy(arg0 context.Context, arg1 string, arg2 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DecrBy", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// DecrBy indicates an expected call of DecrBy.
+func (mr *MockCmdableMockRecorder) DecrBy(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecrBy", reflect.TypeOf((*MockCmdable)(nil).DecrBy), arg0, arg1, arg2)
+}
+
+// Del mocks base method.
+func (m *MockCmdable) Del(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Del", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Del indicates an expected call of Del.
+func (mr *MockCmdableMockRecorder) Del(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockCmdable)(nil).Del), varargs...)
+}
+
+// Dump mocks base method.
+func (m *MockCmdable) Dump(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Dump", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// Dump indicates an expected call of Dump.
+func (mr *MockCmdableMockRecorder) Dump(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dump", reflect.TypeOf((*MockCmdable)(nil).Dump), arg0, arg1)
+}
+
+// Echo mocks base method.
+func (m *MockCmdable) Echo(arg0 context.Context, arg1 interface{}) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Echo", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// Echo indicates an expected call of Echo.
+func (mr *MockCmdableMockRecorder) Echo(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Echo", reflect.TypeOf((*MockCmdable)(nil).Echo), arg0, arg1)
+}
+
+// Eval mocks base method.
+func (m *MockCmdable) Eval(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Eval", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// Eval indicates an expected call of Eval.
+func (mr *MockCmdableMockRecorder) Eval(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eval", reflect.TypeOf((*MockCmdable)(nil).Eval), varargs...)
+}
+
+// EvalRO mocks base method.
+func (m *MockCmdable) EvalRO(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "EvalRO", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// EvalRO indicates an expected call of EvalRO.
+func (mr *MockCmdableMockRecorder) EvalRO(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalRO", reflect.TypeOf((*MockCmdable)(nil).EvalRO), varargs...)
+}
+
+// EvalSha mocks base method.
+func (m *MockCmdable) EvalSha(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "EvalSha", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// EvalSha indicates an expected call of EvalSha.
+func (mr *MockCmdableMockRecorder) EvalSha(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalSha", reflect.TypeOf((*MockCmdable)(nil).EvalSha), varargs...)
+}
+
+// EvalShaRO mocks base method.
+func (m *MockCmdable) EvalShaRO(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "EvalShaRO", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// EvalShaRO indicates an expected call of EvalShaRO.
+func (mr *MockCmdableMockRecorder) EvalShaRO(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalShaRO", reflect.TypeOf((*MockCmdable)(nil).EvalShaRO), varargs...)
+}
+
+// Exists mocks base method.
+func (m *MockCmdable) Exists(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Exists", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Exists indicates an expected call of Exists.
+func (mr *MockCmdableMockRecorder) Exists(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockCmdable)(nil).Exists), varargs...)
+}
+
+// Expire mocks base method.
+func (m *MockCmdable) Expire(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Expire", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// Expire indicates an expected call of Expire.
+func (mr *MockCmdableMockRecorder) Expire(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Expire", reflect.TypeOf((*MockCmdable)(nil).Expire), arg0, arg1, arg2)
+}
+
+// ExpireAt mocks base method.
+func (m *MockCmdable) ExpireAt(arg0 context.Context, arg1 string, arg2 time.Time) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireAt", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ExpireAt indicates an expected call of ExpireAt.
+func (mr *MockCmdableMockRecorder) ExpireAt(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireAt", reflect.TypeOf((*MockCmdable)(nil).ExpireAt), arg0, arg1, arg2)
+}
+
+// ExpireGT mocks base method.
+func (m *MockCmdable) ExpireGT(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireGT", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ExpireGT indicates an expected call of ExpireGT.
+func (mr *MockCmdableMockRecorder) ExpireGT(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireGT", reflect.TypeOf((*MockCmdable)(nil).ExpireGT), arg0, arg1, arg2)
+}
+
+// ExpireLT mocks base method.
+func (m *MockCmdable) ExpireLT(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireLT", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ExpireLT indicates an expected call of ExpireLT.
+func (mr *MockCmdableMockRecorder) ExpireLT(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireLT", reflect.TypeOf((*MockCmdable)(nil).ExpireLT), arg0, arg1, arg2)
+}
+
+// ExpireNX mocks base method.
+func (m *MockCmdable) ExpireNX(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireNX", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ExpireNX indicates an expected call of ExpireNX.
+func (mr *MockCmdableMockRecorder) ExpireNX(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireNX", reflect.TypeOf((*MockCmdable)(nil).ExpireNX), arg0, arg1, arg2)
+}
+
+// ExpireTime mocks base method.
+func (m *MockCmdable) ExpireTime(arg0 context.Context, arg1 string) *redis.DurationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireTime", arg0, arg1)
+ ret0, _ := ret[0].(*redis.DurationCmd)
+ return ret0
+}
+
+// ExpireTime indicates an expected call of ExpireTime.
+func (mr *MockCmdableMockRecorder) ExpireTime(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireTime", reflect.TypeOf((*MockCmdable)(nil).ExpireTime), arg0, arg1)
+}
+
+// ExpireXX mocks base method.
+func (m *MockCmdable) ExpireXX(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExpireXX", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// ExpireXX indicates an expected call of ExpireXX.
+func (mr *MockCmdableMockRecorder) ExpireXX(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireXX", reflect.TypeOf((*MockCmdable)(nil).ExpireXX), arg0, arg1, arg2)
+}
+
+// FCall mocks base method.
+func (m *MockCmdable) FCall(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "FCall", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// FCall indicates an expected call of FCall.
+func (mr *MockCmdableMockRecorder) FCall(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCall", reflect.TypeOf((*MockCmdable)(nil).FCall), varargs...)
+}
+
+// FCallRO mocks base method.
+func (m *MockCmdable) FCallRO(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "FCallRO", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// FCallRO indicates an expected call of FCallRO.
+func (mr *MockCmdableMockRecorder) FCallRO(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCallRO", reflect.TypeOf((*MockCmdable)(nil).FCallRO), varargs...)
+}
+
+// FCallRo mocks base method.
+func (m *MockCmdable) FCallRo(arg0 context.Context, arg1 string, arg2 []string, arg3 ...interface{}) *redis.Cmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "FCallRo", varargs...)
+ ret0, _ := ret[0].(*redis.Cmd)
+ return ret0
+}
+
+// FCallRo indicates an expected call of FCallRo.
+func (mr *MockCmdableMockRecorder) FCallRo(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCallRo", reflect.TypeOf((*MockCmdable)(nil).FCallRo), varargs...)
+}
+
+// FlushAll mocks base method.
+func (m *MockCmdable) FlushAll(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FlushAll", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// FlushAll indicates an expected call of FlushAll.
+func (mr *MockCmdableMockRecorder) FlushAll(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushAll", reflect.TypeOf((*MockCmdable)(nil).FlushAll), arg0)
+}
+
+// FlushAllAsync mocks base method.
+func (m *MockCmdable) FlushAllAsync(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FlushAllAsync", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// FlushAllAsync indicates an expected call of FlushAllAsync.
+func (mr *MockCmdableMockRecorder) FlushAllAsync(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushAllAsync", reflect.TypeOf((*MockCmdable)(nil).FlushAllAsync), arg0)
+}
+
+// FlushDB mocks base method.
+func (m *MockCmdable) FlushDB(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FlushDB", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// FlushDB indicates an expected call of FlushDB.
+func (mr *MockCmdableMockRecorder) FlushDB(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDB", reflect.TypeOf((*MockCmdable)(nil).FlushDB), arg0)
+}
+
+// FlushDBAsync mocks base method.
+func (m *MockCmdable) FlushDBAsync(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FlushDBAsync", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// FlushDBAsync indicates an expected call of FlushDBAsync.
+func (mr *MockCmdableMockRecorder) FlushDBAsync(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDBAsync", reflect.TypeOf((*MockCmdable)(nil).FlushDBAsync), arg0)
+}
+
+// FunctionDelete mocks base method.
+func (m *MockCmdable) FunctionDelete(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionDelete", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionDelete indicates an expected call of FunctionDelete.
+func (mr *MockCmdableMockRecorder) FunctionDelete(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionDelete", reflect.TypeOf((*MockCmdable)(nil).FunctionDelete), arg0, arg1)
+}
+
+// FunctionDump mocks base method.
+func (m *MockCmdable) FunctionDump(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionDump", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionDump indicates an expected call of FunctionDump.
+func (mr *MockCmdableMockRecorder) FunctionDump(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionDump", reflect.TypeOf((*MockCmdable)(nil).FunctionDump), arg0)
+}
+
+// FunctionFlush mocks base method.
+func (m *MockCmdable) FunctionFlush(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionFlush", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionFlush indicates an expected call of FunctionFlush.
+func (mr *MockCmdableMockRecorder) FunctionFlush(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionFlush", reflect.TypeOf((*MockCmdable)(nil).FunctionFlush), arg0)
+}
+
+// FunctionFlushAsync mocks base method.
+func (m *MockCmdable) FunctionFlushAsync(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionFlushAsync", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionFlushAsync indicates an expected call of FunctionFlushAsync.
+func (mr *MockCmdableMockRecorder) FunctionFlushAsync(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionFlushAsync", reflect.TypeOf((*MockCmdable)(nil).FunctionFlushAsync), arg0)
+}
+
+// FunctionKill mocks base method.
+func (m *MockCmdable) FunctionKill(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionKill", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionKill indicates an expected call of FunctionKill.
+func (mr *MockCmdableMockRecorder) FunctionKill(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionKill", reflect.TypeOf((*MockCmdable)(nil).FunctionKill), arg0)
+}
+
+// FunctionList mocks base method.
+func (m *MockCmdable) FunctionList(arg0 context.Context, arg1 redis.FunctionListQuery) *redis.FunctionListCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionList", arg0, arg1)
+ ret0, _ := ret[0].(*redis.FunctionListCmd)
+ return ret0
+}
+
+// FunctionList indicates an expected call of FunctionList.
+func (mr *MockCmdableMockRecorder) FunctionList(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionList", reflect.TypeOf((*MockCmdable)(nil).FunctionList), arg0, arg1)
+}
+
+// FunctionLoad mocks base method.
+func (m *MockCmdable) FunctionLoad(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionLoad", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionLoad indicates an expected call of FunctionLoad.
+func (mr *MockCmdableMockRecorder) FunctionLoad(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionLoad", reflect.TypeOf((*MockCmdable)(nil).FunctionLoad), arg0, arg1)
+}
+
+// FunctionLoadReplace mocks base method.
+func (m *MockCmdable) FunctionLoadReplace(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionLoadReplace", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionLoadReplace indicates an expected call of FunctionLoadReplace.
+func (mr *MockCmdableMockRecorder) FunctionLoadReplace(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionLoadReplace", reflect.TypeOf((*MockCmdable)(nil).FunctionLoadReplace), arg0, arg1)
+}
+
+// FunctionRestore mocks base method.
+func (m *MockCmdable) FunctionRestore(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionRestore", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// FunctionRestore indicates an expected call of FunctionRestore.
+func (mr *MockCmdableMockRecorder) FunctionRestore(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionRestore", reflect.TypeOf((*MockCmdable)(nil).FunctionRestore), arg0, arg1)
+}
+
+// FunctionStats mocks base method.
+func (m *MockCmdable) FunctionStats(arg0 context.Context) *redis.FunctionStatsCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FunctionStats", arg0)
+ ret0, _ := ret[0].(*redis.FunctionStatsCmd)
+ return ret0
+}
+
+// FunctionStats indicates an expected call of FunctionStats.
+func (mr *MockCmdableMockRecorder) FunctionStats(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionStats", reflect.TypeOf((*MockCmdable)(nil).FunctionStats), arg0)
+}
+
+// GeoAdd mocks base method.
+func (m *MockCmdable) GeoAdd(arg0 context.Context, arg1 string, arg2 ...*redis.GeoLocation) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GeoAdd", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// GeoAdd indicates an expected call of GeoAdd.
+func (mr *MockCmdableMockRecorder) GeoAdd(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoAdd", reflect.TypeOf((*MockCmdable)(nil).GeoAdd), varargs...)
+}
+
+// GeoDist mocks base method.
+func (m *MockCmdable) GeoDist(arg0 context.Context, arg1, arg2, arg3, arg4 string) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoDist", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// GeoDist indicates an expected call of GeoDist.
+func (mr *MockCmdableMockRecorder) GeoDist(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoDist", reflect.TypeOf((*MockCmdable)(nil).GeoDist), arg0, arg1, arg2, arg3, arg4)
+}
+
+// GeoHash mocks base method.
+func (m *MockCmdable) GeoHash(arg0 context.Context, arg1 string, arg2 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GeoHash", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// GeoHash indicates an expected call of GeoHash.
+func (mr *MockCmdableMockRecorder) GeoHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoHash", reflect.TypeOf((*MockCmdable)(nil).GeoHash), varargs...)
+}
+
+// GeoPos mocks base method.
+func (m *MockCmdable) GeoPos(arg0 context.Context, arg1 string, arg2 ...string) *redis.GeoPosCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GeoPos", varargs...)
+ ret0, _ := ret[0].(*redis.GeoPosCmd)
+ return ret0
+}
+
+// GeoPos indicates an expected call of GeoPos.
+func (mr *MockCmdableMockRecorder) GeoPos(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoPos", reflect.TypeOf((*MockCmdable)(nil).GeoPos), varargs...)
+}
+
+// GeoRadius mocks base method.
+func (m *MockCmdable) GeoRadius(arg0 context.Context, arg1 string, arg2, arg3 float64, arg4 *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoRadius", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.GeoLocationCmd)
+ return ret0
+}
+
+// GeoRadius indicates an expected call of GeoRadius.
+func (mr *MockCmdableMockRecorder) GeoRadius(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadius", reflect.TypeOf((*MockCmdable)(nil).GeoRadius), arg0, arg1, arg2, arg3, arg4)
+}
+
+// GeoRadiusByMember mocks base method.
+func (m *MockCmdable) GeoRadiusByMember(arg0 context.Context, arg1, arg2 string, arg3 *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoRadiusByMember", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.GeoLocationCmd)
+ return ret0
+}
+
+// GeoRadiusByMember indicates an expected call of GeoRadiusByMember.
+func (mr *MockCmdableMockRecorder) GeoRadiusByMember(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusByMember", reflect.TypeOf((*MockCmdable)(nil).GeoRadiusByMember), arg0, arg1, arg2, arg3)
+}
+
+// GeoRadiusByMemberStore mocks base method.
+func (m *MockCmdable) GeoRadiusByMemberStore(arg0 context.Context, arg1, arg2 string, arg3 *redis.GeoRadiusQuery) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoRadiusByMemberStore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// GeoRadiusByMemberStore indicates an expected call of GeoRadiusByMemberStore.
+func (mr *MockCmdableMockRecorder) GeoRadiusByMemberStore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusByMemberStore", reflect.TypeOf((*MockCmdable)(nil).GeoRadiusByMemberStore), arg0, arg1, arg2, arg3)
+}
+
+// GeoRadiusStore mocks base method.
+func (m *MockCmdable) GeoRadiusStore(arg0 context.Context, arg1 string, arg2, arg3 float64, arg4 *redis.GeoRadiusQuery) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoRadiusStore", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// GeoRadiusStore indicates an expected call of GeoRadiusStore.
+func (mr *MockCmdableMockRecorder) GeoRadiusStore(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusStore", reflect.TypeOf((*MockCmdable)(nil).GeoRadiusStore), arg0, arg1, arg2, arg3, arg4)
+}
+
+// GeoSearch mocks base method.
+func (m *MockCmdable) GeoSearch(arg0 context.Context, arg1 string, arg2 *redis.GeoSearchQuery) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoSearch", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// GeoSearch indicates an expected call of GeoSearch.
+func (mr *MockCmdableMockRecorder) GeoSearch(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearch", reflect.TypeOf((*MockCmdable)(nil).GeoSearch), arg0, arg1, arg2)
+}
+
+// GeoSearchLocation mocks base method.
+func (m *MockCmdable) GeoSearchLocation(arg0 context.Context, arg1 string, arg2 *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoSearchLocation", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.GeoSearchLocationCmd)
+ return ret0
+}
+
+// GeoSearchLocation indicates an expected call of GeoSearchLocation.
+func (mr *MockCmdableMockRecorder) GeoSearchLocation(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearchLocation", reflect.TypeOf((*MockCmdable)(nil).GeoSearchLocation), arg0, arg1, arg2)
+}
+
+// GeoSearchStore mocks base method.
+func (m *MockCmdable) GeoSearchStore(arg0 context.Context, arg1, arg2 string, arg3 *redis.GeoSearchStoreQuery) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GeoSearchStore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// GeoSearchStore indicates an expected call of GeoSearchStore.
+func (mr *MockCmdableMockRecorder) GeoSearchStore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearchStore", reflect.TypeOf((*MockCmdable)(nil).GeoSearchStore), arg0, arg1, arg2, arg3)
+}
+
+// Get mocks base method.
+func (m *MockCmdable) Get(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockCmdableMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCmdable)(nil).Get), arg0, arg1)
+}
+
+// GetBit mocks base method.
+func (m *MockCmdable) GetBit(arg0 context.Context, arg1 string, arg2 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBit", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// GetBit indicates an expected call of GetBit.
+func (mr *MockCmdableMockRecorder) GetBit(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBit", reflect.TypeOf((*MockCmdable)(nil).GetBit), arg0, arg1, arg2)
+}
+
+// GetDel mocks base method.
+func (m *MockCmdable) GetDel(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetDel", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// GetDel indicates an expected call of GetDel.
+func (mr *MockCmdableMockRecorder) GetDel(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDel", reflect.TypeOf((*MockCmdable)(nil).GetDel), arg0, arg1)
+}
+
+// GetEx mocks base method.
+func (m *MockCmdable) GetEx(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetEx", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// GetEx indicates an expected call of GetEx.
+func (mr *MockCmdableMockRecorder) GetEx(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEx", reflect.TypeOf((*MockCmdable)(nil).GetEx), arg0, arg1, arg2)
+}
+
+// GetRange mocks base method.
+func (m *MockCmdable) GetRange(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// GetRange indicates an expected call of GetRange.
+func (mr *MockCmdableMockRecorder) GetRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRange", reflect.TypeOf((*MockCmdable)(nil).GetRange), arg0, arg1, arg2, arg3)
+}
+
+// GetSet mocks base method.
+func (m *MockCmdable) GetSet(arg0 context.Context, arg1 string, arg2 interface{}) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetSet", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// GetSet indicates an expected call of GetSet.
+func (mr *MockCmdableMockRecorder) GetSet(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSet", reflect.TypeOf((*MockCmdable)(nil).GetSet), arg0, arg1, arg2)
+}
+
+// HDel mocks base method.
+func (m *MockCmdable) HDel(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "HDel", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// HDel indicates an expected call of HDel.
+func (mr *MockCmdableMockRecorder) HDel(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HDel", reflect.TypeOf((*MockCmdable)(nil).HDel), varargs...)
+}
+
+// HExists mocks base method.
+func (m *MockCmdable) HExists(arg0 context.Context, arg1, arg2 string) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HExists", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// HExists indicates an expected call of HExists.
+func (mr *MockCmdableMockRecorder) HExists(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExists", reflect.TypeOf((*MockCmdable)(nil).HExists), arg0, arg1, arg2)
+}
+
+// HGet mocks base method.
+func (m *MockCmdable) HGet(arg0 context.Context, arg1, arg2 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HGet", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// HGet indicates an expected call of HGet.
+func (mr *MockCmdableMockRecorder) HGet(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGet", reflect.TypeOf((*MockCmdable)(nil).HGet), arg0, arg1, arg2)
+}
+
+// HGetAll mocks base method.
+func (m *MockCmdable) HGetAll(arg0 context.Context, arg1 string) *redis.MapStringStringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HGetAll", arg0, arg1)
+ ret0, _ := ret[0].(*redis.MapStringStringCmd)
+ return ret0
+}
+
+// HGetAll indicates an expected call of HGetAll.
+func (mr *MockCmdableMockRecorder) HGetAll(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGetAll", reflect.TypeOf((*MockCmdable)(nil).HGetAll), arg0, arg1)
+}
+
+// HIncrBy mocks base method.
+func (m *MockCmdable) HIncrBy(arg0 context.Context, arg1, arg2 string, arg3 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HIncrBy", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// HIncrBy indicates an expected call of HIncrBy.
+func (mr *MockCmdableMockRecorder) HIncrBy(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HIncrBy", reflect.TypeOf((*MockCmdable)(nil).HIncrBy), arg0, arg1, arg2, arg3)
+}
+
+// HIncrByFloat mocks base method.
+func (m *MockCmdable) HIncrByFloat(arg0 context.Context, arg1, arg2 string, arg3 float64) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HIncrByFloat", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// HIncrByFloat indicates an expected call of HIncrByFloat.
+func (mr *MockCmdableMockRecorder) HIncrByFloat(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HIncrByFloat", reflect.TypeOf((*MockCmdable)(nil).HIncrByFloat), arg0, arg1, arg2, arg3)
+}
+
+// HKeys mocks base method.
+func (m *MockCmdable) HKeys(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HKeys", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// HKeys indicates an expected call of HKeys.
+func (mr *MockCmdableMockRecorder) HKeys(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HKeys", reflect.TypeOf((*MockCmdable)(nil).HKeys), arg0, arg1)
+}
+
+// HLen mocks base method.
+func (m *MockCmdable) HLen(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HLen", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// HLen indicates an expected call of HLen.
+func (mr *MockCmdableMockRecorder) HLen(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HLen", reflect.TypeOf((*MockCmdable)(nil).HLen), arg0, arg1)
+}
+
+// HMGet mocks base method.
+func (m *MockCmdable) HMGet(arg0 context.Context, arg1 string, arg2 ...string) *redis.SliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "HMGet", varargs...)
+ ret0, _ := ret[0].(*redis.SliceCmd)
+ return ret0
+}
+
+// HMGet indicates an expected call of HMGet.
+func (mr *MockCmdableMockRecorder) HMGet(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HMGet", reflect.TypeOf((*MockCmdable)(nil).HMGet), varargs...)
+}
+
+// HMSet mocks base method.
+func (m *MockCmdable) HMSet(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "HMSet", varargs...)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// HMSet indicates an expected call of HMSet.
+func (mr *MockCmdableMockRecorder) HMSet(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HMSet", reflect.TypeOf((*MockCmdable)(nil).HMSet), varargs...)
+}
+
+// HRandField mocks base method.
+func (m *MockCmdable) HRandField(arg0 context.Context, arg1 string, arg2 int) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HRandField", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// HRandField indicates an expected call of HRandField.
+func (mr *MockCmdableMockRecorder) HRandField(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HRandField", reflect.TypeOf((*MockCmdable)(nil).HRandField), arg0, arg1, arg2)
+}
+
+// HRandFieldWithValues mocks base method.
+func (m *MockCmdable) HRandFieldWithValues(arg0 context.Context, arg1 string, arg2 int) *redis.KeyValueSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HRandFieldWithValues", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.KeyValueSliceCmd)
+ return ret0
+}
+
+// HRandFieldWithValues indicates an expected call of HRandFieldWithValues.
+func (mr *MockCmdableMockRecorder) HRandFieldWithValues(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HRandFieldWithValues", reflect.TypeOf((*MockCmdable)(nil).HRandFieldWithValues), arg0, arg1, arg2)
+}
+
+// HScan mocks base method.
+func (m *MockCmdable) HScan(arg0 context.Context, arg1 string, arg2 uint64, arg3 string, arg4 int64) *redis.ScanCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HScan", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.ScanCmd)
+ return ret0
+}
+
+// HScan indicates an expected call of HScan.
+func (mr *MockCmdableMockRecorder) HScan(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HScan", reflect.TypeOf((*MockCmdable)(nil).HScan), arg0, arg1, arg2, arg3, arg4)
+}
+
+// HSet mocks base method.
+func (m *MockCmdable) HSet(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "HSet", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// HSet indicates an expected call of HSet.
+func (mr *MockCmdableMockRecorder) HSet(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSet", reflect.TypeOf((*MockCmdable)(nil).HSet), varargs...)
+}
+
+// HSetNX mocks base method.
+func (m *MockCmdable) HSetNX(arg0 context.Context, arg1, arg2 string, arg3 interface{}) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HSetNX", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// HSetNX indicates an expected call of HSetNX.
+func (mr *MockCmdableMockRecorder) HSetNX(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSetNX", reflect.TypeOf((*MockCmdable)(nil).HSetNX), arg0, arg1, arg2, arg3)
+}
+
+// HVals mocks base method.
+func (m *MockCmdable) HVals(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HVals", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// HVals indicates an expected call of HVals.
+func (mr *MockCmdableMockRecorder) HVals(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HVals", reflect.TypeOf((*MockCmdable)(nil).HVals), arg0, arg1)
+}
+
+// Incr mocks base method.
+func (m *MockCmdable) Incr(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Incr", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Incr indicates an expected call of Incr.
+func (mr *MockCmdableMockRecorder) Incr(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Incr", reflect.TypeOf((*MockCmdable)(nil).Incr), arg0, arg1)
+}
+
+// IncrBy mocks base method.
+func (m *MockCmdable) IncrBy(arg0 context.Context, arg1 string, arg2 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IncrBy", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// IncrBy indicates an expected call of IncrBy.
+func (mr *MockCmdableMockRecorder) IncrBy(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrBy", reflect.TypeOf((*MockCmdable)(nil).IncrBy), arg0, arg1, arg2)
+}
+
+// IncrByFloat mocks base method.
+func (m *MockCmdable) IncrByFloat(arg0 context.Context, arg1 string, arg2 float64) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IncrByFloat", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// IncrByFloat indicates an expected call of IncrByFloat.
+func (mr *MockCmdableMockRecorder) IncrByFloat(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrByFloat", reflect.TypeOf((*MockCmdable)(nil).IncrByFloat), arg0, arg1, arg2)
+}
+
+// Info mocks base method.
+func (m *MockCmdable) Info(arg0 context.Context, arg1 ...string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Info", varargs...)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *MockCmdableMockRecorder) Info(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockCmdable)(nil).Info), varargs...)
+}
+
+// Keys mocks base method.
+func (m *MockCmdable) Keys(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Keys", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// Keys indicates an expected call of Keys.
+func (mr *MockCmdableMockRecorder) Keys(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Keys", reflect.TypeOf((*MockCmdable)(nil).Keys), arg0, arg1)
+}
+
+// LCS mocks base method.
+func (m *MockCmdable) LCS(arg0 context.Context, arg1 *redis.LCSQuery) *redis.LCSCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LCS", arg0, arg1)
+ ret0, _ := ret[0].(*redis.LCSCmd)
+ return ret0
+}
+
+// LCS indicates an expected call of LCS.
+func (mr *MockCmdableMockRecorder) LCS(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LCS", reflect.TypeOf((*MockCmdable)(nil).LCS), arg0, arg1)
+}
+
+// LIndex mocks base method.
+func (m *MockCmdable) LIndex(arg0 context.Context, arg1 string, arg2 int64) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LIndex", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// LIndex indicates an expected call of LIndex.
+func (mr *MockCmdableMockRecorder) LIndex(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LIndex", reflect.TypeOf((*MockCmdable)(nil).LIndex), arg0, arg1, arg2)
+}
+
+// LInsert mocks base method.
+func (m *MockCmdable) LInsert(arg0 context.Context, arg1, arg2 string, arg3, arg4 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LInsert", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LInsert indicates an expected call of LInsert.
+func (mr *MockCmdableMockRecorder) LInsert(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsert", reflect.TypeOf((*MockCmdable)(nil).LInsert), arg0, arg1, arg2, arg3, arg4)
+}
+
+// LInsertAfter mocks base method.
+func (m *MockCmdable) LInsertAfter(arg0 context.Context, arg1 string, arg2, arg3 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LInsertAfter", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LInsertAfter indicates an expected call of LInsertAfter.
+func (mr *MockCmdableMockRecorder) LInsertAfter(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsertAfter", reflect.TypeOf((*MockCmdable)(nil).LInsertAfter), arg0, arg1, arg2, arg3)
+}
+
+// LInsertBefore mocks base method.
+func (m *MockCmdable) LInsertBefore(arg0 context.Context, arg1 string, arg2, arg3 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LInsertBefore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LInsertBefore indicates an expected call of LInsertBefore.
+func (mr *MockCmdableMockRecorder) LInsertBefore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsertBefore", reflect.TypeOf((*MockCmdable)(nil).LInsertBefore), arg0, arg1, arg2, arg3)
+}
+
+// LLen mocks base method.
+func (m *MockCmdable) LLen(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LLen", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LLen indicates an expected call of LLen.
+func (mr *MockCmdableMockRecorder) LLen(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LLen", reflect.TypeOf((*MockCmdable)(nil).LLen), arg0, arg1)
+}
+
+// LMPop mocks base method.
+func (m *MockCmdable) LMPop(arg0 context.Context, arg1 string, arg2 int64, arg3 ...string) *redis.KeyValuesCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "LMPop", varargs...)
+ ret0, _ := ret[0].(*redis.KeyValuesCmd)
+ return ret0
+}
+
+// LMPop indicates an expected call of LMPop.
+func (mr *MockCmdableMockRecorder) LMPop(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LMPop", reflect.TypeOf((*MockCmdable)(nil).LMPop), varargs...)
+}
+
+// LMove mocks base method.
+func (m *MockCmdable) LMove(arg0 context.Context, arg1, arg2, arg3, arg4 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LMove", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// LMove indicates an expected call of LMove.
+func (mr *MockCmdableMockRecorder) LMove(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LMove", reflect.TypeOf((*MockCmdable)(nil).LMove), arg0, arg1, arg2, arg3, arg4)
+}
+
+// LPop mocks base method.
+func (m *MockCmdable) LPop(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LPop", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// LPop indicates an expected call of LPop.
+func (mr *MockCmdableMockRecorder) LPop(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPop", reflect.TypeOf((*MockCmdable)(nil).LPop), arg0, arg1)
+}
+
+// LPopCount mocks base method.
+func (m *MockCmdable) LPopCount(arg0 context.Context, arg1 string, arg2 int) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LPopCount", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// LPopCount indicates an expected call of LPopCount.
+func (mr *MockCmdableMockRecorder) LPopCount(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPopCount", reflect.TypeOf((*MockCmdable)(nil).LPopCount), arg0, arg1, arg2)
+}
+
+// LPos mocks base method.
+func (m *MockCmdable) LPos(arg0 context.Context, arg1, arg2 string, arg3 redis.LPosArgs) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LPos", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LPos indicates an expected call of LPos.
+func (mr *MockCmdableMockRecorder) LPos(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPos", reflect.TypeOf((*MockCmdable)(nil).LPos), arg0, arg1, arg2, arg3)
+}
+
+// LPosCount mocks base method.
+func (m *MockCmdable) LPosCount(arg0 context.Context, arg1, arg2 string, arg3 int64, arg4 redis.LPosArgs) *redis.IntSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LPosCount", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.IntSliceCmd)
+ return ret0
+}
+
+// LPosCount indicates an expected call of LPosCount.
+func (mr *MockCmdableMockRecorder) LPosCount(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPosCount", reflect.TypeOf((*MockCmdable)(nil).LPosCount), arg0, arg1, arg2, arg3, arg4)
+}
+
+// LPush mocks base method.
+func (m *MockCmdable) LPush(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "LPush", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LPush indicates an expected call of LPush.
+func (mr *MockCmdableMockRecorder) LPush(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPush", reflect.TypeOf((*MockCmdable)(nil).LPush), varargs...)
+}
+
+// LPushX mocks base method.
+func (m *MockCmdable) LPushX(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "LPushX", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LPushX indicates an expected call of LPushX.
+func (mr *MockCmdableMockRecorder) LPushX(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPushX", reflect.TypeOf((*MockCmdable)(nil).LPushX), varargs...)
+}
+
+// LRange mocks base method.
+func (m *MockCmdable) LRange(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// LRange indicates an expected call of LRange.
+func (mr *MockCmdableMockRecorder) LRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LRange", reflect.TypeOf((*MockCmdable)(nil).LRange), arg0, arg1, arg2, arg3)
+}
+
+// LRem mocks base method.
+func (m *MockCmdable) LRem(arg0 context.Context, arg1 string, arg2 int64, arg3 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LRem", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LRem indicates an expected call of LRem.
+func (mr *MockCmdableMockRecorder) LRem(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LRem", reflect.TypeOf((*MockCmdable)(nil).LRem), arg0, arg1, arg2, arg3)
+}
+
+// LSet mocks base method.
+func (m *MockCmdable) LSet(arg0 context.Context, arg1 string, arg2 int64, arg3 interface{}) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LSet", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// LSet indicates an expected call of LSet.
+func (mr *MockCmdableMockRecorder) LSet(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LSet", reflect.TypeOf((*MockCmdable)(nil).LSet), arg0, arg1, arg2, arg3)
+}
+
+// LTrim mocks base method.
+func (m *MockCmdable) LTrim(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LTrim", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// LTrim indicates an expected call of LTrim.
+func (mr *MockCmdableMockRecorder) LTrim(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LTrim", reflect.TypeOf((*MockCmdable)(nil).LTrim), arg0, arg1, arg2, arg3)
+}
+
+// LastSave mocks base method.
+func (m *MockCmdable) LastSave(arg0 context.Context) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LastSave", arg0)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// LastSave indicates an expected call of LastSave.
+func (mr *MockCmdableMockRecorder) LastSave(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastSave", reflect.TypeOf((*MockCmdable)(nil).LastSave), arg0)
+}
+
+// MGet mocks base method.
+func (m *MockCmdable) MGet(arg0 context.Context, arg1 ...string) *redis.SliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "MGet", varargs...)
+ ret0, _ := ret[0].(*redis.SliceCmd)
+ return ret0
+}
+
+// MGet indicates an expected call of MGet.
+func (mr *MockCmdableMockRecorder) MGet(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGet", reflect.TypeOf((*MockCmdable)(nil).MGet), varargs...)
+}
+
+// MSet mocks base method.
+func (m *MockCmdable) MSet(arg0 context.Context, arg1 ...interface{}) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "MSet", varargs...)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// MSet indicates an expected call of MSet.
+func (mr *MockCmdableMockRecorder) MSet(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MSet", reflect.TypeOf((*MockCmdable)(nil).MSet), varargs...)
+}
+
+// MSetNX mocks base method.
+func (m *MockCmdable) MSetNX(arg0 context.Context, arg1 ...interface{}) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "MSetNX", varargs...)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// MSetNX indicates an expected call of MSetNX.
+func (mr *MockCmdableMockRecorder) MSetNX(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MSetNX", reflect.TypeOf((*MockCmdable)(nil).MSetNX), varargs...)
+}
+
+// MemoryUsage mocks base method.
+func (m *MockCmdable) MemoryUsage(arg0 context.Context, arg1 string, arg2 ...int) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "MemoryUsage", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// MemoryUsage indicates an expected call of MemoryUsage.
+func (mr *MockCmdableMockRecorder) MemoryUsage(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MemoryUsage", reflect.TypeOf((*MockCmdable)(nil).MemoryUsage), varargs...)
+}
+
+// Migrate mocks base method.
+func (m *MockCmdable) Migrate(arg0 context.Context, arg1, arg2, arg3 string, arg4 int, arg5 time.Duration) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Migrate", arg0, arg1, arg2, arg3, arg4, arg5)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Migrate indicates an expected call of Migrate.
+func (mr *MockCmdableMockRecorder) Migrate(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockCmdable)(nil).Migrate), arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+// ModuleLoadex mocks base method.
+func (m *MockCmdable) ModuleLoadex(arg0 context.Context, arg1 *redis.ModuleLoadexConfig) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ModuleLoadex", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ModuleLoadex indicates an expected call of ModuleLoadex.
+func (mr *MockCmdableMockRecorder) ModuleLoadex(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModuleLoadex", reflect.TypeOf((*MockCmdable)(nil).ModuleLoadex), arg0, arg1)
+}
+
+// Move mocks base method.
+func (m *MockCmdable) Move(arg0 context.Context, arg1 string, arg2 int) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Move", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// Move indicates an expected call of Move.
+func (mr *MockCmdableMockRecorder) Move(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Move", reflect.TypeOf((*MockCmdable)(nil).Move), arg0, arg1, arg2)
+}
+
+// ObjectEncoding mocks base method.
+func (m *MockCmdable) ObjectEncoding(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ObjectEncoding", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ObjectEncoding indicates an expected call of ObjectEncoding.
+func (mr *MockCmdableMockRecorder) ObjectEncoding(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEncoding", reflect.TypeOf((*MockCmdable)(nil).ObjectEncoding), arg0, arg1)
+}
+
+// ObjectIdleTime mocks base method.
+func (m *MockCmdable) ObjectIdleTime(arg0 context.Context, arg1 string) *redis.DurationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ObjectIdleTime", arg0, arg1)
+ ret0, _ := ret[0].(*redis.DurationCmd)
+ return ret0
+}
+
+// ObjectIdleTime indicates an expected call of ObjectIdleTime.
+func (mr *MockCmdableMockRecorder) ObjectIdleTime(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectIdleTime", reflect.TypeOf((*MockCmdable)(nil).ObjectIdleTime), arg0, arg1)
+}
+
+// ObjectRefCount mocks base method.
+func (m *MockCmdable) ObjectRefCount(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ObjectRefCount", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ObjectRefCount indicates an expected call of ObjectRefCount.
+func (mr *MockCmdableMockRecorder) ObjectRefCount(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectRefCount", reflect.TypeOf((*MockCmdable)(nil).ObjectRefCount), arg0, arg1)
+}
+
+// PExpire mocks base method.
+func (m *MockCmdable) PExpire(arg0 context.Context, arg1 string, arg2 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PExpire", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// PExpire indicates an expected call of PExpire.
+func (mr *MockCmdableMockRecorder) PExpire(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpire", reflect.TypeOf((*MockCmdable)(nil).PExpire), arg0, arg1, arg2)
+}
+
+// PExpireAt mocks base method.
+func (m *MockCmdable) PExpireAt(arg0 context.Context, arg1 string, arg2 time.Time) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PExpireAt", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// PExpireAt indicates an expected call of PExpireAt.
+func (mr *MockCmdableMockRecorder) PExpireAt(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpireAt", reflect.TypeOf((*MockCmdable)(nil).PExpireAt), arg0, arg1, arg2)
+}
+
+// PExpireTime mocks base method.
+func (m *MockCmdable) PExpireTime(arg0 context.Context, arg1 string) *redis.DurationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PExpireTime", arg0, arg1)
+ ret0, _ := ret[0].(*redis.DurationCmd)
+ return ret0
+}
+
+// PExpireTime indicates an expected call of PExpireTime.
+func (mr *MockCmdableMockRecorder) PExpireTime(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpireTime", reflect.TypeOf((*MockCmdable)(nil).PExpireTime), arg0, arg1)
+}
+
+// PFAdd mocks base method.
+func (m *MockCmdable) PFAdd(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PFAdd", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// PFAdd indicates an expected call of PFAdd.
+func (mr *MockCmdableMockRecorder) PFAdd(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFAdd", reflect.TypeOf((*MockCmdable)(nil).PFAdd), varargs...)
+}
+
+// PFCount mocks base method.
+func (m *MockCmdable) PFCount(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PFCount", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// PFCount indicates an expected call of PFCount.
+func (mr *MockCmdableMockRecorder) PFCount(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFCount", reflect.TypeOf((*MockCmdable)(nil).PFCount), varargs...)
+}
+
+// PFMerge mocks base method.
+func (m *MockCmdable) PFMerge(arg0 context.Context, arg1 string, arg2 ...string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PFMerge", varargs...)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// PFMerge indicates an expected call of PFMerge.
+func (mr *MockCmdableMockRecorder) PFMerge(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFMerge", reflect.TypeOf((*MockCmdable)(nil).PFMerge), varargs...)
+}
+
+// PTTL mocks base method.
+func (m *MockCmdable) PTTL(arg0 context.Context, arg1 string) *redis.DurationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PTTL", arg0, arg1)
+ ret0, _ := ret[0].(*redis.DurationCmd)
+ return ret0
+}
+
+// PTTL indicates an expected call of PTTL.
+func (mr *MockCmdableMockRecorder) PTTL(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PTTL", reflect.TypeOf((*MockCmdable)(nil).PTTL), arg0, arg1)
+}
+
+// Persist mocks base method.
+func (m *MockCmdable) Persist(arg0 context.Context, arg1 string) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Persist", arg0, arg1)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// Persist indicates an expected call of Persist.
+func (mr *MockCmdableMockRecorder) Persist(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Persist", reflect.TypeOf((*MockCmdable)(nil).Persist), arg0, arg1)
+}
+
+// Ping mocks base method.
+func (m *MockCmdable) Ping(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Ping", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Ping indicates an expected call of Ping.
+func (mr *MockCmdableMockRecorder) Ping(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockCmdable)(nil).Ping), arg0)
+}
+
+// Pipeline mocks base method.
+func (m *MockCmdable) Pipeline() redis.Pipeliner {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Pipeline")
+ ret0, _ := ret[0].(redis.Pipeliner)
+ return ret0
+}
+
+// Pipeline indicates an expected call of Pipeline.
+func (mr *MockCmdableMockRecorder) Pipeline() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipeline", reflect.TypeOf((*MockCmdable)(nil).Pipeline))
+}
+
+// Pipelined mocks base method.
+func (m *MockCmdable) Pipelined(arg0 context.Context, arg1 func(redis.Pipeliner) error) ([]redis.Cmder, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Pipelined", arg0, arg1)
+ ret0, _ := ret[0].([]redis.Cmder)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Pipelined indicates an expected call of Pipelined.
+func (mr *MockCmdableMockRecorder) Pipelined(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipelined", reflect.TypeOf((*MockCmdable)(nil).Pipelined), arg0, arg1)
+}
+
+// PubSubChannels mocks base method.
+func (m *MockCmdable) PubSubChannels(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PubSubChannels", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// PubSubChannels indicates an expected call of PubSubChannels.
+func (mr *MockCmdableMockRecorder) PubSubChannels(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubChannels", reflect.TypeOf((*MockCmdable)(nil).PubSubChannels), arg0, arg1)
+}
+
+// PubSubNumPat mocks base method.
+func (m *MockCmdable) PubSubNumPat(arg0 context.Context) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PubSubNumPat", arg0)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// PubSubNumPat indicates an expected call of PubSubNumPat.
+func (mr *MockCmdableMockRecorder) PubSubNumPat(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubNumPat", reflect.TypeOf((*MockCmdable)(nil).PubSubNumPat), arg0)
+}
+
+// PubSubNumSub mocks base method.
+func (m *MockCmdable) PubSubNumSub(arg0 context.Context, arg1 ...string) *redis.MapStringIntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PubSubNumSub", varargs...)
+ ret0, _ := ret[0].(*redis.MapStringIntCmd)
+ return ret0
+}
+
+// PubSubNumSub indicates an expected call of PubSubNumSub.
+func (mr *MockCmdableMockRecorder) PubSubNumSub(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubNumSub", reflect.TypeOf((*MockCmdable)(nil).PubSubNumSub), varargs...)
+}
+
+// PubSubShardChannels mocks base method.
+func (m *MockCmdable) PubSubShardChannels(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PubSubShardChannels", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// PubSubShardChannels indicates an expected call of PubSubShardChannels.
+func (mr *MockCmdableMockRecorder) PubSubShardChannels(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubShardChannels", reflect.TypeOf((*MockCmdable)(nil).PubSubShardChannels), arg0, arg1)
+}
+
+// PubSubShardNumSub mocks base method.
+func (m *MockCmdable) PubSubShardNumSub(arg0 context.Context, arg1 ...string) *redis.MapStringIntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PubSubShardNumSub", varargs...)
+ ret0, _ := ret[0].(*redis.MapStringIntCmd)
+ return ret0
+}
+
+// PubSubShardNumSub indicates an expected call of PubSubShardNumSub.
+func (mr *MockCmdableMockRecorder) PubSubShardNumSub(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubShardNumSub", reflect.TypeOf((*MockCmdable)(nil).PubSubShardNumSub), varargs...)
+}
+
+// Publish mocks base method.
+func (m *MockCmdable) Publish(arg0 context.Context, arg1 string, arg2 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Publish", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Publish indicates an expected call of Publish.
+func (mr *MockCmdableMockRecorder) Publish(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockCmdable)(nil).Publish), arg0, arg1, arg2)
+}
+
+// Quit mocks base method.
+func (m *MockCmdable) Quit(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Quit", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Quit indicates an expected call of Quit.
+func (mr *MockCmdableMockRecorder) Quit(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Quit", reflect.TypeOf((*MockCmdable)(nil).Quit), arg0)
+}
+
+// RPop mocks base method.
+func (m *MockCmdable) RPop(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RPop", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// RPop indicates an expected call of RPop.
+func (mr *MockCmdableMockRecorder) RPop(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPop", reflect.TypeOf((*MockCmdable)(nil).RPop), arg0, arg1)
+}
+
+// RPopCount mocks base method.
+func (m *MockCmdable) RPopCount(arg0 context.Context, arg1 string, arg2 int) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RPopCount", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// RPopCount indicates an expected call of RPopCount.
+func (mr *MockCmdableMockRecorder) RPopCount(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPopCount", reflect.TypeOf((*MockCmdable)(nil).RPopCount), arg0, arg1, arg2)
+}
+
+// RPopLPush mocks base method.
+func (m *MockCmdable) RPopLPush(arg0 context.Context, arg1, arg2 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RPopLPush", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// RPopLPush indicates an expected call of RPopLPush.
+func (mr *MockCmdableMockRecorder) RPopLPush(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPopLPush", reflect.TypeOf((*MockCmdable)(nil).RPopLPush), arg0, arg1, arg2)
+}
+
+// RPush mocks base method.
+func (m *MockCmdable) RPush(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "RPush", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// RPush indicates an expected call of RPush.
+func (mr *MockCmdableMockRecorder) RPush(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPush", reflect.TypeOf((*MockCmdable)(nil).RPush), varargs...)
+}
+
+// RPushX mocks base method.
+func (m *MockCmdable) RPushX(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "RPushX", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// RPushX indicates an expected call of RPushX.
+func (mr *MockCmdableMockRecorder) RPushX(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPushX", reflect.TypeOf((*MockCmdable)(nil).RPushX), varargs...)
+}
+
+// RandomKey mocks base method.
+func (m *MockCmdable) RandomKey(arg0 context.Context) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RandomKey", arg0)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// RandomKey indicates an expected call of RandomKey.
+func (mr *MockCmdableMockRecorder) RandomKey(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RandomKey", reflect.TypeOf((*MockCmdable)(nil).RandomKey), arg0)
+}
+
+// ReadOnly mocks base method.
+func (m *MockCmdable) ReadOnly(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ReadOnly", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ReadOnly indicates an expected call of ReadOnly.
+func (mr *MockCmdableMockRecorder) ReadOnly(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadOnly", reflect.TypeOf((*MockCmdable)(nil).ReadOnly), arg0)
+}
+
+// ReadWrite mocks base method.
+func (m *MockCmdable) ReadWrite(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ReadWrite", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ReadWrite indicates an expected call of ReadWrite.
+func (mr *MockCmdableMockRecorder) ReadWrite(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWrite", reflect.TypeOf((*MockCmdable)(nil).ReadWrite), arg0)
+}
+
+// Rename mocks base method.
+func (m *MockCmdable) Rename(arg0 context.Context, arg1, arg2 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Rename", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Rename indicates an expected call of Rename.
+func (mr *MockCmdableMockRecorder) Rename(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockCmdable)(nil).Rename), arg0, arg1, arg2)
+}
+
+// RenameNX mocks base method.
+func (m *MockCmdable) RenameNX(arg0 context.Context, arg1, arg2 string) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RenameNX", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// RenameNX indicates an expected call of RenameNX.
+func (mr *MockCmdableMockRecorder) RenameNX(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenameNX", reflect.TypeOf((*MockCmdable)(nil).RenameNX), arg0, arg1, arg2)
+}
+
+// Restore mocks base method.
+func (m *MockCmdable) Restore(arg0 context.Context, arg1 string, arg2 time.Duration, arg3 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Restore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Restore indicates an expected call of Restore.
+func (mr *MockCmdableMockRecorder) Restore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restore", reflect.TypeOf((*MockCmdable)(nil).Restore), arg0, arg1, arg2, arg3)
+}
+
+// RestoreReplace mocks base method.
+func (m *MockCmdable) RestoreReplace(arg0 context.Context, arg1 string, arg2 time.Duration, arg3 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RestoreReplace", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// RestoreReplace indicates an expected call of RestoreReplace.
+func (mr *MockCmdableMockRecorder) RestoreReplace(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreReplace", reflect.TypeOf((*MockCmdable)(nil).RestoreReplace), arg0, arg1, arg2, arg3)
+}
+
+// SAdd mocks base method.
+func (m *MockCmdable) SAdd(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SAdd", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SAdd indicates an expected call of SAdd.
+func (mr *MockCmdableMockRecorder) SAdd(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SAdd", reflect.TypeOf((*MockCmdable)(nil).SAdd), varargs...)
+}
+
+// SCard mocks base method.
+func (m *MockCmdable) SCard(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SCard", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SCard indicates an expected call of SCard.
+func (mr *MockCmdableMockRecorder) SCard(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SCard", reflect.TypeOf((*MockCmdable)(nil).SCard), arg0, arg1)
+}
+
+// SDiff mocks base method.
+func (m *MockCmdable) SDiff(arg0 context.Context, arg1 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SDiff", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SDiff indicates an expected call of SDiff.
+func (mr *MockCmdableMockRecorder) SDiff(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SDiff", reflect.TypeOf((*MockCmdable)(nil).SDiff), varargs...)
+}
+
+// SDiffStore mocks base method.
+func (m *MockCmdable) SDiffStore(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SDiffStore", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SDiffStore indicates an expected call of SDiffStore.
+func (mr *MockCmdableMockRecorder) SDiffStore(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SDiffStore", reflect.TypeOf((*MockCmdable)(nil).SDiffStore), varargs...)
+}
+
+// SInter mocks base method.
+func (m *MockCmdable) SInter(arg0 context.Context, arg1 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SInter", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SInter indicates an expected call of SInter.
+func (mr *MockCmdableMockRecorder) SInter(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInter", reflect.TypeOf((*MockCmdable)(nil).SInter), varargs...)
+}
+
+// SInterCard mocks base method.
+func (m *MockCmdable) SInterCard(arg0 context.Context, arg1 int64, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SInterCard", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SInterCard indicates an expected call of SInterCard.
+func (mr *MockCmdableMockRecorder) SInterCard(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInterCard", reflect.TypeOf((*MockCmdable)(nil).SInterCard), varargs...)
+}
+
+// SInterStore mocks base method.
+func (m *MockCmdable) SInterStore(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SInterStore", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SInterStore indicates an expected call of SInterStore.
+func (mr *MockCmdableMockRecorder) SInterStore(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInterStore", reflect.TypeOf((*MockCmdable)(nil).SInterStore), varargs...)
+}
+
+// SIsMember mocks base method.
+func (m *MockCmdable) SIsMember(arg0 context.Context, arg1 string, arg2 interface{}) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SIsMember", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// SIsMember indicates an expected call of SIsMember.
+func (mr *MockCmdableMockRecorder) SIsMember(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SIsMember", reflect.TypeOf((*MockCmdable)(nil).SIsMember), arg0, arg1, arg2)
+}
+
+// SMIsMember mocks base method.
+func (m *MockCmdable) SMIsMember(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.BoolSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SMIsMember", varargs...)
+ ret0, _ := ret[0].(*redis.BoolSliceCmd)
+ return ret0
+}
+
+// SMIsMember indicates an expected call of SMIsMember.
+func (mr *MockCmdableMockRecorder) SMIsMember(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMIsMember", reflect.TypeOf((*MockCmdable)(nil).SMIsMember), varargs...)
+}
+
+// SMembers mocks base method.
+func (m *MockCmdable) SMembers(arg0 context.Context, arg1 string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SMembers", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SMembers indicates an expected call of SMembers.
+func (mr *MockCmdableMockRecorder) SMembers(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMembers", reflect.TypeOf((*MockCmdable)(nil).SMembers), arg0, arg1)
+}
+
+// SMembersMap mocks base method.
+func (m *MockCmdable) SMembersMap(arg0 context.Context, arg1 string) *redis.StringStructMapCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SMembersMap", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringStructMapCmd)
+ return ret0
+}
+
+// SMembersMap indicates an expected call of SMembersMap.
+func (mr *MockCmdableMockRecorder) SMembersMap(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMembersMap", reflect.TypeOf((*MockCmdable)(nil).SMembersMap), arg0, arg1)
+}
+
+// SMove mocks base method.
+func (m *MockCmdable) SMove(arg0 context.Context, arg1, arg2 string, arg3 interface{}) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SMove", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// SMove indicates an expected call of SMove.
+func (mr *MockCmdableMockRecorder) SMove(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMove", reflect.TypeOf((*MockCmdable)(nil).SMove), arg0, arg1, arg2, arg3)
+}
+
+// SPop mocks base method.
+func (m *MockCmdable) SPop(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SPop", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// SPop indicates an expected call of SPop.
+func (mr *MockCmdableMockRecorder) SPop(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPop", reflect.TypeOf((*MockCmdable)(nil).SPop), arg0, arg1)
+}
+
+// SPopN mocks base method.
+func (m *MockCmdable) SPopN(arg0 context.Context, arg1 string, arg2 int64) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SPopN", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SPopN indicates an expected call of SPopN.
+func (mr *MockCmdableMockRecorder) SPopN(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPopN", reflect.TypeOf((*MockCmdable)(nil).SPopN), arg0, arg1, arg2)
+}
+
+// SPublish mocks base method.
+func (m *MockCmdable) SPublish(arg0 context.Context, arg1 string, arg2 interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SPublish", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SPublish indicates an expected call of SPublish.
+func (mr *MockCmdableMockRecorder) SPublish(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPublish", reflect.TypeOf((*MockCmdable)(nil).SPublish), arg0, arg1, arg2)
+}
+
+// SRandMember mocks base method.
+func (m *MockCmdable) SRandMember(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SRandMember", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// SRandMember indicates an expected call of SRandMember.
+func (mr *MockCmdableMockRecorder) SRandMember(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRandMember", reflect.TypeOf((*MockCmdable)(nil).SRandMember), arg0, arg1)
+}
+
+// SRandMemberN mocks base method.
+func (m *MockCmdable) SRandMemberN(arg0 context.Context, arg1 string, arg2 int64) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SRandMemberN", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SRandMemberN indicates an expected call of SRandMemberN.
+func (mr *MockCmdableMockRecorder) SRandMemberN(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRandMemberN", reflect.TypeOf((*MockCmdable)(nil).SRandMemberN), arg0, arg1, arg2)
+}
+
+// SRem mocks base method.
+func (m *MockCmdable) SRem(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SRem", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SRem indicates an expected call of SRem.
+func (mr *MockCmdableMockRecorder) SRem(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRem", reflect.TypeOf((*MockCmdable)(nil).SRem), varargs...)
+}
+
+// SScan mocks base method.
+func (m *MockCmdable) SScan(arg0 context.Context, arg1 string, arg2 uint64, arg3 string, arg4 int64) *redis.ScanCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SScan", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.ScanCmd)
+ return ret0
+}
+
+// SScan indicates an expected call of SScan.
+func (mr *MockCmdableMockRecorder) SScan(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SScan", reflect.TypeOf((*MockCmdable)(nil).SScan), arg0, arg1, arg2, arg3, arg4)
+}
+
+// SUnion mocks base method.
+func (m *MockCmdable) SUnion(arg0 context.Context, arg1 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SUnion", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SUnion indicates an expected call of SUnion.
+func (mr *MockCmdableMockRecorder) SUnion(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SUnion", reflect.TypeOf((*MockCmdable)(nil).SUnion), varargs...)
+}
+
+// SUnionStore mocks base method.
+func (m *MockCmdable) SUnionStore(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SUnionStore", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SUnionStore indicates an expected call of SUnionStore.
+func (mr *MockCmdableMockRecorder) SUnionStore(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SUnionStore", reflect.TypeOf((*MockCmdable)(nil).SUnionStore), varargs...)
+}
+
+// Save mocks base method.
+func (m *MockCmdable) Save(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Save indicates an expected call of Save.
+func (mr *MockCmdableMockRecorder) Save(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockCmdable)(nil).Save), arg0)
+}
+
+// Scan mocks base method.
+func (m *MockCmdable) Scan(arg0 context.Context, arg1 uint64, arg2 string, arg3 int64) *redis.ScanCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Scan", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.ScanCmd)
+ return ret0
+}
+
+// Scan indicates an expected call of Scan.
+func (mr *MockCmdableMockRecorder) Scan(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockCmdable)(nil).Scan), arg0, arg1, arg2, arg3)
+}
+
+// ScanType mocks base method.
+func (m *MockCmdable) ScanType(arg0 context.Context, arg1 uint64, arg2 string, arg3 int64, arg4 string) *redis.ScanCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ScanType", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.ScanCmd)
+ return ret0
+}
+
+// ScanType indicates an expected call of ScanType.
+func (mr *MockCmdableMockRecorder) ScanType(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanType", reflect.TypeOf((*MockCmdable)(nil).ScanType), arg0, arg1, arg2, arg3, arg4)
+}
+
+// ScriptExists mocks base method.
+func (m *MockCmdable) ScriptExists(arg0 context.Context, arg1 ...string) *redis.BoolSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ScriptExists", varargs...)
+ ret0, _ := ret[0].(*redis.BoolSliceCmd)
+ return ret0
+}
+
+// ScriptExists indicates an expected call of ScriptExists.
+func (mr *MockCmdableMockRecorder) ScriptExists(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptExists", reflect.TypeOf((*MockCmdable)(nil).ScriptExists), varargs...)
+}
+
+// ScriptFlush mocks base method.
+func (m *MockCmdable) ScriptFlush(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ScriptFlush", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ScriptFlush indicates an expected call of ScriptFlush.
+func (mr *MockCmdableMockRecorder) ScriptFlush(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptFlush", reflect.TypeOf((*MockCmdable)(nil).ScriptFlush), arg0)
+}
+
+// ScriptKill mocks base method.
+func (m *MockCmdable) ScriptKill(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ScriptKill", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ScriptKill indicates an expected call of ScriptKill.
+func (mr *MockCmdableMockRecorder) ScriptKill(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptKill", reflect.TypeOf((*MockCmdable)(nil).ScriptKill), arg0)
+}
+
+// ScriptLoad mocks base method.
+func (m *MockCmdable) ScriptLoad(arg0 context.Context, arg1 string) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ScriptLoad", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// ScriptLoad indicates an expected call of ScriptLoad.
+func (mr *MockCmdableMockRecorder) ScriptLoad(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptLoad", reflect.TypeOf((*MockCmdable)(nil).ScriptLoad), arg0, arg1)
+}
+
+// Set mocks base method.
+func (m *MockCmdable) Set(arg0 context.Context, arg1 string, arg2 interface{}, arg3 time.Duration) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Set", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Set indicates an expected call of Set.
+func (mr *MockCmdableMockRecorder) Set(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockCmdable)(nil).Set), arg0, arg1, arg2, arg3)
+}
+
+// SetArgs mocks base method.
+func (m *MockCmdable) SetArgs(arg0 context.Context, arg1 string, arg2 interface{}, arg3 redis.SetArgs) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetArgs", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// SetArgs indicates an expected call of SetArgs.
+func (mr *MockCmdableMockRecorder) SetArgs(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArgs", reflect.TypeOf((*MockCmdable)(nil).SetArgs), arg0, arg1, arg2, arg3)
+}
+
+// SetBit mocks base method.
+func (m *MockCmdable) SetBit(arg0 context.Context, arg1 string, arg2 int64, arg3 int) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetBit", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SetBit indicates an expected call of SetBit.
+func (mr *MockCmdableMockRecorder) SetBit(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBit", reflect.TypeOf((*MockCmdable)(nil).SetBit), arg0, arg1, arg2, arg3)
+}
+
+// SetEx mocks base method.
+func (m *MockCmdable) SetEx(arg0 context.Context, arg1 string, arg2 interface{}, arg3 time.Duration) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetEx", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// SetEx indicates an expected call of SetEx.
+func (mr *MockCmdableMockRecorder) SetEx(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEx", reflect.TypeOf((*MockCmdable)(nil).SetEx), arg0, arg1, arg2, arg3)
+}
+
+// SetNX mocks base method.
+func (m *MockCmdable) SetNX(arg0 context.Context, arg1 string, arg2 interface{}, arg3 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetNX", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// SetNX indicates an expected call of SetNX.
+func (mr *MockCmdableMockRecorder) SetNX(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNX", reflect.TypeOf((*MockCmdable)(nil).SetNX), arg0, arg1, arg2, arg3)
+}
+
+// SetRange mocks base method.
+func (m *MockCmdable) SetRange(arg0 context.Context, arg1 string, arg2 int64, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SetRange indicates an expected call of SetRange.
+func (mr *MockCmdableMockRecorder) SetRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRange", reflect.TypeOf((*MockCmdable)(nil).SetRange), arg0, arg1, arg2, arg3)
+}
+
+// SetXX mocks base method.
+func (m *MockCmdable) SetXX(arg0 context.Context, arg1 string, arg2 interface{}, arg3 time.Duration) *redis.BoolCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetXX", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.BoolCmd)
+ return ret0
+}
+
+// SetXX indicates an expected call of SetXX.
+func (mr *MockCmdableMockRecorder) SetXX(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetXX", reflect.TypeOf((*MockCmdable)(nil).SetXX), arg0, arg1, arg2, arg3)
+}
+
+// Shutdown mocks base method.
+func (m *MockCmdable) Shutdown(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Shutdown", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Shutdown indicates an expected call of Shutdown.
+func (mr *MockCmdableMockRecorder) Shutdown(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockCmdable)(nil).Shutdown), arg0)
+}
+
+// ShutdownNoSave mocks base method.
+func (m *MockCmdable) ShutdownNoSave(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ShutdownNoSave", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ShutdownNoSave indicates an expected call of ShutdownNoSave.
+func (mr *MockCmdableMockRecorder) ShutdownNoSave(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownNoSave", reflect.TypeOf((*MockCmdable)(nil).ShutdownNoSave), arg0)
+}
+
+// ShutdownSave mocks base method.
+func (m *MockCmdable) ShutdownSave(arg0 context.Context) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ShutdownSave", arg0)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// ShutdownSave indicates an expected call of ShutdownSave.
+func (mr *MockCmdableMockRecorder) ShutdownSave(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownSave", reflect.TypeOf((*MockCmdable)(nil).ShutdownSave), arg0)
+}
+
+// SlaveOf mocks base method.
+func (m *MockCmdable) SlaveOf(arg0 context.Context, arg1, arg2 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SlaveOf", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// SlaveOf indicates an expected call of SlaveOf.
+func (mr *MockCmdableMockRecorder) SlaveOf(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlaveOf", reflect.TypeOf((*MockCmdable)(nil).SlaveOf), arg0, arg1, arg2)
+}
+
+// SlowLogGet mocks base method.
+func (m *MockCmdable) SlowLogGet(arg0 context.Context, arg1 int64) *redis.SlowLogCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SlowLogGet", arg0, arg1)
+ ret0, _ := ret[0].(*redis.SlowLogCmd)
+ return ret0
+}
+
+// SlowLogGet indicates an expected call of SlowLogGet.
+func (mr *MockCmdableMockRecorder) SlowLogGet(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlowLogGet", reflect.TypeOf((*MockCmdable)(nil).SlowLogGet), arg0, arg1)
+}
+
+// Sort mocks base method.
+func (m *MockCmdable) Sort(arg0 context.Context, arg1 string, arg2 *redis.Sort) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Sort", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// Sort indicates an expected call of Sort.
+func (mr *MockCmdableMockRecorder) Sort(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sort", reflect.TypeOf((*MockCmdable)(nil).Sort), arg0, arg1, arg2)
+}
+
+// SortInterfaces mocks base method.
+func (m *MockCmdable) SortInterfaces(arg0 context.Context, arg1 string, arg2 *redis.Sort) *redis.SliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SortInterfaces", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.SliceCmd)
+ return ret0
+}
+
+// SortInterfaces indicates an expected call of SortInterfaces.
+func (mr *MockCmdableMockRecorder) SortInterfaces(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortInterfaces", reflect.TypeOf((*MockCmdable)(nil).SortInterfaces), arg0, arg1, arg2)
+}
+
+// SortRO mocks base method.
+func (m *MockCmdable) SortRO(arg0 context.Context, arg1 string, arg2 *redis.Sort) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SortRO", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// SortRO indicates an expected call of SortRO.
+func (mr *MockCmdableMockRecorder) SortRO(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortRO", reflect.TypeOf((*MockCmdable)(nil).SortRO), arg0, arg1, arg2)
+}
+
+// SortStore mocks base method.
+func (m *MockCmdable) SortStore(arg0 context.Context, arg1, arg2 string, arg3 *redis.Sort) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SortStore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// SortStore indicates an expected call of SortStore.
+func (mr *MockCmdableMockRecorder) SortStore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortStore", reflect.TypeOf((*MockCmdable)(nil).SortStore), arg0, arg1, arg2, arg3)
+}
+
+// StrLen mocks base method.
+func (m *MockCmdable) StrLen(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StrLen", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// StrLen indicates an expected call of StrLen.
+func (mr *MockCmdableMockRecorder) StrLen(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StrLen", reflect.TypeOf((*MockCmdable)(nil).StrLen), arg0, arg1)
+}
+
+// TTL mocks base method.
+func (m *MockCmdable) TTL(arg0 context.Context, arg1 string) *redis.DurationCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "TTL", arg0, arg1)
+ ret0, _ := ret[0].(*redis.DurationCmd)
+ return ret0
+}
+
+// TTL indicates an expected call of TTL.
+func (mr *MockCmdableMockRecorder) TTL(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TTL", reflect.TypeOf((*MockCmdable)(nil).TTL), arg0, arg1)
+}
+
+// Time mocks base method.
+func (m *MockCmdable) Time(arg0 context.Context) *redis.TimeCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Time", arg0)
+ ret0, _ := ret[0].(*redis.TimeCmd)
+ return ret0
+}
+
+// Time indicates an expected call of Time.
+func (mr *MockCmdableMockRecorder) Time(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Time", reflect.TypeOf((*MockCmdable)(nil).Time), arg0)
+}
+
+// Touch mocks base method.
+func (m *MockCmdable) Touch(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Touch", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Touch indicates an expected call of Touch.
+func (mr *MockCmdableMockRecorder) Touch(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Touch", reflect.TypeOf((*MockCmdable)(nil).Touch), varargs...)
+}
+
+// TxPipeline mocks base method.
+func (m *MockCmdable) TxPipeline() redis.Pipeliner {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "TxPipeline")
+ ret0, _ := ret[0].(redis.Pipeliner)
+ return ret0
+}
+
+// TxPipeline indicates an expected call of TxPipeline.
+func (mr *MockCmdableMockRecorder) TxPipeline() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPipeline", reflect.TypeOf((*MockCmdable)(nil).TxPipeline))
+}
+
+// TxPipelined mocks base method.
+func (m *MockCmdable) TxPipelined(arg0 context.Context, arg1 func(redis.Pipeliner) error) ([]redis.Cmder, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "TxPipelined", arg0, arg1)
+ ret0, _ := ret[0].([]redis.Cmder)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// TxPipelined indicates an expected call of TxPipelined.
+func (mr *MockCmdableMockRecorder) TxPipelined(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPipelined", reflect.TypeOf((*MockCmdable)(nil).TxPipelined), arg0, arg1)
+}
+
+// Type mocks base method.
+func (m *MockCmdable) Type(arg0 context.Context, arg1 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Type", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// Type indicates an expected call of Type.
+func (mr *MockCmdableMockRecorder) Type(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockCmdable)(nil).Type), arg0, arg1)
+}
+
+// Unlink mocks base method.
+func (m *MockCmdable) Unlink(arg0 context.Context, arg1 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Unlink", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// Unlink indicates an expected call of Unlink.
+func (mr *MockCmdableMockRecorder) Unlink(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlink", reflect.TypeOf((*MockCmdable)(nil).Unlink), varargs...)
+}
+
+// XAck mocks base method.
+func (m *MockCmdable) XAck(arg0 context.Context, arg1, arg2 string, arg3 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "XAck", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XAck indicates an expected call of XAck.
+func (mr *MockCmdableMockRecorder) XAck(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAck", reflect.TypeOf((*MockCmdable)(nil).XAck), varargs...)
+}
+
+// XAdd mocks base method.
+func (m *MockCmdable) XAdd(arg0 context.Context, arg1 *redis.XAddArgs) *redis.StringCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XAdd", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringCmd)
+ return ret0
+}
+
+// XAdd indicates an expected call of XAdd.
+func (mr *MockCmdableMockRecorder) XAdd(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAdd", reflect.TypeOf((*MockCmdable)(nil).XAdd), arg0, arg1)
+}
+
+// XAutoClaim mocks base method.
+func (m *MockCmdable) XAutoClaim(arg0 context.Context, arg1 *redis.XAutoClaimArgs) *redis.XAutoClaimCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XAutoClaim", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XAutoClaimCmd)
+ return ret0
+}
+
+// XAutoClaim indicates an expected call of XAutoClaim.
+func (mr *MockCmdableMockRecorder) XAutoClaim(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAutoClaim", reflect.TypeOf((*MockCmdable)(nil).XAutoClaim), arg0, arg1)
+}
+
+// XAutoClaimJustID mocks base method.
+func (m *MockCmdable) XAutoClaimJustID(arg0 context.Context, arg1 *redis.XAutoClaimArgs) *redis.XAutoClaimJustIDCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XAutoClaimJustID", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XAutoClaimJustIDCmd)
+ return ret0
+}
+
+// XAutoClaimJustID indicates an expected call of XAutoClaimJustID.
+func (mr *MockCmdableMockRecorder) XAutoClaimJustID(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAutoClaimJustID", reflect.TypeOf((*MockCmdable)(nil).XAutoClaimJustID), arg0, arg1)
+}
+
+// XClaim mocks base method.
+func (m *MockCmdable) XClaim(arg0 context.Context, arg1 *redis.XClaimArgs) *redis.XMessageSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XClaim", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XMessageSliceCmd)
+ return ret0
+}
+
+// XClaim indicates an expected call of XClaim.
+func (mr *MockCmdableMockRecorder) XClaim(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XClaim", reflect.TypeOf((*MockCmdable)(nil).XClaim), arg0, arg1)
+}
+
+// XClaimJustID mocks base method.
+func (m *MockCmdable) XClaimJustID(arg0 context.Context, arg1 *redis.XClaimArgs) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XClaimJustID", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// XClaimJustID indicates an expected call of XClaimJustID.
+func (mr *MockCmdableMockRecorder) XClaimJustID(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XClaimJustID", reflect.TypeOf((*MockCmdable)(nil).XClaimJustID), arg0, arg1)
+}
+
+// XDel mocks base method.
+func (m *MockCmdable) XDel(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "XDel", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XDel indicates an expected call of XDel.
+func (mr *MockCmdableMockRecorder) XDel(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XDel", reflect.TypeOf((*MockCmdable)(nil).XDel), varargs...)
+}
+
+// XGroupCreate mocks base method.
+func (m *MockCmdable) XGroupCreate(arg0 context.Context, arg1, arg2, arg3 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupCreate", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// XGroupCreate indicates an expected call of XGroupCreate.
+func (mr *MockCmdableMockRecorder) XGroupCreate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreate", reflect.TypeOf((*MockCmdable)(nil).XGroupCreate), arg0, arg1, arg2, arg3)
+}
+
+// XGroupCreateConsumer mocks base method.
+func (m *MockCmdable) XGroupCreateConsumer(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupCreateConsumer", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XGroupCreateConsumer indicates an expected call of XGroupCreateConsumer.
+func (mr *MockCmdableMockRecorder) XGroupCreateConsumer(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreateConsumer", reflect.TypeOf((*MockCmdable)(nil).XGroupCreateConsumer), arg0, arg1, arg2, arg3)
+}
+
+// XGroupCreateMkStream mocks base method.
+func (m *MockCmdable) XGroupCreateMkStream(arg0 context.Context, arg1, arg2, arg3 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupCreateMkStream", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// XGroupCreateMkStream indicates an expected call of XGroupCreateMkStream.
+func (mr *MockCmdableMockRecorder) XGroupCreateMkStream(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreateMkStream", reflect.TypeOf((*MockCmdable)(nil).XGroupCreateMkStream), arg0, arg1, arg2, arg3)
+}
+
+// XGroupDelConsumer mocks base method.
+func (m *MockCmdable) XGroupDelConsumer(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupDelConsumer", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XGroupDelConsumer indicates an expected call of XGroupDelConsumer.
+func (mr *MockCmdableMockRecorder) XGroupDelConsumer(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupDelConsumer", reflect.TypeOf((*MockCmdable)(nil).XGroupDelConsumer), arg0, arg1, arg2, arg3)
+}
+
+// XGroupDestroy mocks base method.
+func (m *MockCmdable) XGroupDestroy(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupDestroy", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XGroupDestroy indicates an expected call of XGroupDestroy.
+func (mr *MockCmdableMockRecorder) XGroupDestroy(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupDestroy", reflect.TypeOf((*MockCmdable)(nil).XGroupDestroy), arg0, arg1, arg2)
+}
+
+// XGroupSetID mocks base method.
+func (m *MockCmdable) XGroupSetID(arg0 context.Context, arg1, arg2, arg3 string) *redis.StatusCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XGroupSetID", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StatusCmd)
+ return ret0
+}
+
+// XGroupSetID indicates an expected call of XGroupSetID.
+func (mr *MockCmdableMockRecorder) XGroupSetID(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupSetID", reflect.TypeOf((*MockCmdable)(nil).XGroupSetID), arg0, arg1, arg2, arg3)
+}
+
+// XInfoConsumers mocks base method.
+func (m *MockCmdable) XInfoConsumers(arg0 context.Context, arg1, arg2 string) *redis.XInfoConsumersCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XInfoConsumers", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.XInfoConsumersCmd)
+ return ret0
+}
+
+// XInfoConsumers indicates an expected call of XInfoConsumers.
+func (mr *MockCmdableMockRecorder) XInfoConsumers(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoConsumers", reflect.TypeOf((*MockCmdable)(nil).XInfoConsumers), arg0, arg1, arg2)
+}
+
+// XInfoGroups mocks base method.
+func (m *MockCmdable) XInfoGroups(arg0 context.Context, arg1 string) *redis.XInfoGroupsCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XInfoGroups", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XInfoGroupsCmd)
+ return ret0
+}
+
+// XInfoGroups indicates an expected call of XInfoGroups.
+func (mr *MockCmdableMockRecorder) XInfoGroups(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoGroups", reflect.TypeOf((*MockCmdable)(nil).XInfoGroups), arg0, arg1)
+}
+
+// XInfoStream mocks base method.
+func (m *MockCmdable) XInfoStream(arg0 context.Context, arg1 string) *redis.XInfoStreamCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XInfoStream", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XInfoStreamCmd)
+ return ret0
+}
+
+// XInfoStream indicates an expected call of XInfoStream.
+func (mr *MockCmdableMockRecorder) XInfoStream(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoStream", reflect.TypeOf((*MockCmdable)(nil).XInfoStream), arg0, arg1)
+}
+
+// XInfoStreamFull mocks base method.
+func (m *MockCmdable) XInfoStreamFull(arg0 context.Context, arg1 string, arg2 int) *redis.XInfoStreamFullCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XInfoStreamFull", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.XInfoStreamFullCmd)
+ return ret0
+}
+
+// XInfoStreamFull indicates an expected call of XInfoStreamFull.
+func (mr *MockCmdableMockRecorder) XInfoStreamFull(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoStreamFull", reflect.TypeOf((*MockCmdable)(nil).XInfoStreamFull), arg0, arg1, arg2)
+}
+
+// XLen mocks base method.
+func (m *MockCmdable) XLen(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XLen", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XLen indicates an expected call of XLen.
+func (mr *MockCmdableMockRecorder) XLen(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XLen", reflect.TypeOf((*MockCmdable)(nil).XLen), arg0, arg1)
+}
+
+// XPending mocks base method.
+func (m *MockCmdable) XPending(arg0 context.Context, arg1, arg2 string) *redis.XPendingCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XPending", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.XPendingCmd)
+ return ret0
+}
+
+// XPending indicates an expected call of XPending.
+func (mr *MockCmdableMockRecorder) XPending(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XPending", reflect.TypeOf((*MockCmdable)(nil).XPending), arg0, arg1, arg2)
+}
+
+// XPendingExt mocks base method.
+func (m *MockCmdable) XPendingExt(arg0 context.Context, arg1 *redis.XPendingExtArgs) *redis.XPendingExtCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XPendingExt", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XPendingExtCmd)
+ return ret0
+}
+
+// XPendingExt indicates an expected call of XPendingExt.
+func (mr *MockCmdableMockRecorder) XPendingExt(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XPendingExt", reflect.TypeOf((*MockCmdable)(nil).XPendingExt), arg0, arg1)
+}
+
+// XRange mocks base method.
+func (m *MockCmdable) XRange(arg0 context.Context, arg1, arg2, arg3 string) *redis.XMessageSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.XMessageSliceCmd)
+ return ret0
+}
+
+// XRange indicates an expected call of XRange.
+func (mr *MockCmdableMockRecorder) XRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRange", reflect.TypeOf((*MockCmdable)(nil).XRange), arg0, arg1, arg2, arg3)
+}
+
+// XRangeN mocks base method.
+func (m *MockCmdable) XRangeN(arg0 context.Context, arg1, arg2, arg3 string, arg4 int64) *redis.XMessageSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XRangeN", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.XMessageSliceCmd)
+ return ret0
+}
+
+// XRangeN indicates an expected call of XRangeN.
+func (mr *MockCmdableMockRecorder) XRangeN(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRangeN", reflect.TypeOf((*MockCmdable)(nil).XRangeN), arg0, arg1, arg2, arg3, arg4)
+}
+
+// XRead mocks base method.
+func (m *MockCmdable) XRead(arg0 context.Context, arg1 *redis.XReadArgs) *redis.XStreamSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XRead", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XStreamSliceCmd)
+ return ret0
+}
+
+// XRead indicates an expected call of XRead.
+func (mr *MockCmdableMockRecorder) XRead(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRead", reflect.TypeOf((*MockCmdable)(nil).XRead), arg0, arg1)
+}
+
+// XReadGroup mocks base method.
+func (m *MockCmdable) XReadGroup(arg0 context.Context, arg1 *redis.XReadGroupArgs) *redis.XStreamSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XReadGroup", arg0, arg1)
+ ret0, _ := ret[0].(*redis.XStreamSliceCmd)
+ return ret0
+}
+
+// XReadGroup indicates an expected call of XReadGroup.
+func (mr *MockCmdableMockRecorder) XReadGroup(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XReadGroup", reflect.TypeOf((*MockCmdable)(nil).XReadGroup), arg0, arg1)
+}
+
+// XReadStreams mocks base method.
+func (m *MockCmdable) XReadStreams(arg0 context.Context, arg1 ...string) *redis.XStreamSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "XReadStreams", varargs...)
+ ret0, _ := ret[0].(*redis.XStreamSliceCmd)
+ return ret0
+}
+
+// XReadStreams indicates an expected call of XReadStreams.
+func (mr *MockCmdableMockRecorder) XReadStreams(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XReadStreams", reflect.TypeOf((*MockCmdable)(nil).XReadStreams), varargs...)
+}
+
+// XRevRange mocks base method.
+func (m *MockCmdable) XRevRange(arg0 context.Context, arg1, arg2, arg3 string) *redis.XMessageSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XRevRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.XMessageSliceCmd)
+ return ret0
+}
+
+// XRevRange indicates an expected call of XRevRange.
+func (mr *MockCmdableMockRecorder) XRevRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRevRange", reflect.TypeOf((*MockCmdable)(nil).XRevRange), arg0, arg1, arg2, arg3)
+}
+
+// XRevRangeN mocks base method.
+func (m *MockCmdable) XRevRangeN(arg0 context.Context, arg1, arg2, arg3 string, arg4 int64) *redis.XMessageSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XRevRangeN", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.XMessageSliceCmd)
+ return ret0
+}
+
+// XRevRangeN indicates an expected call of XRevRangeN.
+func (mr *MockCmdableMockRecorder) XRevRangeN(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRevRangeN", reflect.TypeOf((*MockCmdable)(nil).XRevRangeN), arg0, arg1, arg2, arg3, arg4)
+}
+
+// XTrimMaxLen mocks base method.
+func (m *MockCmdable) XTrimMaxLen(arg0 context.Context, arg1 string, arg2 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XTrimMaxLen", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XTrimMaxLen indicates an expected call of XTrimMaxLen.
+func (mr *MockCmdableMockRecorder) XTrimMaxLen(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLen", reflect.TypeOf((*MockCmdable)(nil).XTrimMaxLen), arg0, arg1, arg2)
+}
+
+// XTrimMaxLenApprox mocks base method.
+func (m *MockCmdable) XTrimMaxLenApprox(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XTrimMaxLenApprox", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XTrimMaxLenApprox indicates an expected call of XTrimMaxLenApprox.
+func (mr *MockCmdableMockRecorder) XTrimMaxLenApprox(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLenApprox", reflect.TypeOf((*MockCmdable)(nil).XTrimMaxLenApprox), arg0, arg1, arg2, arg3)
+}
+
+// XTrimMinID mocks base method.
+func (m *MockCmdable) XTrimMinID(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XTrimMinID", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XTrimMinID indicates an expected call of XTrimMinID.
+func (mr *MockCmdableMockRecorder) XTrimMinID(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinID", reflect.TypeOf((*MockCmdable)(nil).XTrimMinID), arg0, arg1, arg2)
+}
+
+// XTrimMinIDApprox mocks base method.
+func (m *MockCmdable) XTrimMinIDApprox(arg0 context.Context, arg1, arg2 string, arg3 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "XTrimMinIDApprox", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// XTrimMinIDApprox indicates an expected call of XTrimMinIDApprox.
+func (mr *MockCmdableMockRecorder) XTrimMinIDApprox(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinIDApprox", reflect.TypeOf((*MockCmdable)(nil).XTrimMinIDApprox), arg0, arg1, arg2, arg3)
+}
+
+// ZAdd mocks base method.
+func (m *MockCmdable) ZAdd(arg0 context.Context, arg1 string, arg2 ...redis.Z) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZAdd", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAdd indicates an expected call of ZAdd.
+func (mr *MockCmdableMockRecorder) ZAdd(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAdd", reflect.TypeOf((*MockCmdable)(nil).ZAdd), varargs...)
+}
+
+// ZAddArgs mocks base method.
+func (m *MockCmdable) ZAddArgs(arg0 context.Context, arg1 string, arg2 redis.ZAddArgs) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZAddArgs", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAddArgs indicates an expected call of ZAddArgs.
+func (mr *MockCmdableMockRecorder) ZAddArgs(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddArgs", reflect.TypeOf((*MockCmdable)(nil).ZAddArgs), arg0, arg1, arg2)
+}
+
+// ZAddArgsIncr mocks base method.
+func (m *MockCmdable) ZAddArgsIncr(arg0 context.Context, arg1 string, arg2 redis.ZAddArgs) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZAddArgsIncr", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// ZAddArgsIncr indicates an expected call of ZAddArgsIncr.
+func (mr *MockCmdableMockRecorder) ZAddArgsIncr(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddArgsIncr", reflect.TypeOf((*MockCmdable)(nil).ZAddArgsIncr), arg0, arg1, arg2)
+}
+
+// ZAddGT mocks base method.
+func (m *MockCmdable) ZAddGT(arg0 context.Context, arg1 string, arg2 ...redis.Z) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZAddGT", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAddGT indicates an expected call of ZAddGT.
+func (mr *MockCmdableMockRecorder) ZAddGT(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddGT", reflect.TypeOf((*MockCmdable)(nil).ZAddGT), varargs...)
+}
+
+// ZAddLT mocks base method.
+func (m *MockCmdable) ZAddLT(arg0 context.Context, arg1 string, arg2 ...redis.Z) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZAddLT", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAddLT indicates an expected call of ZAddLT.
+func (mr *MockCmdableMockRecorder) ZAddLT(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddLT", reflect.TypeOf((*MockCmdable)(nil).ZAddLT), varargs...)
+}
+
+// ZAddNX mocks base method.
+func (m *MockCmdable) ZAddNX(arg0 context.Context, arg1 string, arg2 ...redis.Z) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZAddNX", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAddNX indicates an expected call of ZAddNX.
+func (mr *MockCmdableMockRecorder) ZAddNX(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddNX", reflect.TypeOf((*MockCmdable)(nil).ZAddNX), varargs...)
+}
+
+// ZAddXX mocks base method.
+func (m *MockCmdable) ZAddXX(arg0 context.Context, arg1 string, arg2 ...redis.Z) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZAddXX", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZAddXX indicates an expected call of ZAddXX.
+func (mr *MockCmdableMockRecorder) ZAddXX(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddXX", reflect.TypeOf((*MockCmdable)(nil).ZAddXX), varargs...)
+}
+
+// ZCard mocks base method.
+func (m *MockCmdable) ZCard(arg0 context.Context, arg1 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZCard", arg0, arg1)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZCard indicates an expected call of ZCard.
+func (mr *MockCmdableMockRecorder) ZCard(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZCard", reflect.TypeOf((*MockCmdable)(nil).ZCard), arg0, arg1)
+}
+
+// ZCount mocks base method.
+func (m *MockCmdable) ZCount(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZCount", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZCount indicates an expected call of ZCount.
+func (mr *MockCmdableMockRecorder) ZCount(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZCount", reflect.TypeOf((*MockCmdable)(nil).ZCount), arg0, arg1, arg2, arg3)
+}
+
+// ZDiff mocks base method.
+func (m *MockCmdable) ZDiff(arg0 context.Context, arg1 ...string) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZDiff", varargs...)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZDiff indicates an expected call of ZDiff.
+func (mr *MockCmdableMockRecorder) ZDiff(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiff", reflect.TypeOf((*MockCmdable)(nil).ZDiff), varargs...)
+}
+
+// ZDiffStore mocks base method.
+func (m *MockCmdable) ZDiffStore(arg0 context.Context, arg1 string, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZDiffStore", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZDiffStore indicates an expected call of ZDiffStore.
+func (mr *MockCmdableMockRecorder) ZDiffStore(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiffStore", reflect.TypeOf((*MockCmdable)(nil).ZDiffStore), varargs...)
+}
+
+// ZDiffWithScores mocks base method.
+func (m *MockCmdable) ZDiffWithScores(arg0 context.Context, arg1 ...string) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZDiffWithScores", varargs...)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZDiffWithScores indicates an expected call of ZDiffWithScores.
+func (mr *MockCmdableMockRecorder) ZDiffWithScores(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiffWithScores", reflect.TypeOf((*MockCmdable)(nil).ZDiffWithScores), varargs...)
+}
+
+// ZIncrBy mocks base method.
+func (m *MockCmdable) ZIncrBy(arg0 context.Context, arg1 string, arg2 float64, arg3 string) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZIncrBy", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// ZIncrBy indicates an expected call of ZIncrBy.
+func (mr *MockCmdableMockRecorder) ZIncrBy(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZIncrBy", reflect.TypeOf((*MockCmdable)(nil).ZIncrBy), arg0, arg1, arg2, arg3)
+}
+
+// ZInter mocks base method.
+func (m *MockCmdable) ZInter(arg0 context.Context, arg1 *redis.ZStore) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZInter", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZInter indicates an expected call of ZInter.
+func (mr *MockCmdableMockRecorder) ZInter(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInter", reflect.TypeOf((*MockCmdable)(nil).ZInter), arg0, arg1)
+}
+
+// ZInterCard mocks base method.
+func (m *MockCmdable) ZInterCard(arg0 context.Context, arg1 int64, arg2 ...string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZInterCard", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZInterCard indicates an expected call of ZInterCard.
+func (mr *MockCmdableMockRecorder) ZInterCard(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterCard", reflect.TypeOf((*MockCmdable)(nil).ZInterCard), varargs...)
+}
+
+// ZInterStore mocks base method.
+func (m *MockCmdable) ZInterStore(arg0 context.Context, arg1 string, arg2 *redis.ZStore) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZInterStore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZInterStore indicates an expected call of ZInterStore.
+func (mr *MockCmdableMockRecorder) ZInterStore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterStore", reflect.TypeOf((*MockCmdable)(nil).ZInterStore), arg0, arg1, arg2)
+}
+
+// ZInterWithScores mocks base method.
+func (m *MockCmdable) ZInterWithScores(arg0 context.Context, arg1 *redis.ZStore) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZInterWithScores", arg0, arg1)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZInterWithScores indicates an expected call of ZInterWithScores.
+func (mr *MockCmdableMockRecorder) ZInterWithScores(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterWithScores", reflect.TypeOf((*MockCmdable)(nil).ZInterWithScores), arg0, arg1)
+}
+
+// ZLexCount mocks base method.
+func (m *MockCmdable) ZLexCount(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZLexCount", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZLexCount indicates an expected call of ZLexCount.
+func (mr *MockCmdableMockRecorder) ZLexCount(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZLexCount", reflect.TypeOf((*MockCmdable)(nil).ZLexCount), arg0, arg1, arg2, arg3)
+}
+
+// ZMPop mocks base method.
+func (m *MockCmdable) ZMPop(arg0 context.Context, arg1 string, arg2 int64, arg3 ...string) *redis.ZSliceWithKeyCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZMPop", varargs...)
+ ret0, _ := ret[0].(*redis.ZSliceWithKeyCmd)
+ return ret0
+}
+
+// ZMPop indicates an expected call of ZMPop.
+func (mr *MockCmdableMockRecorder) ZMPop(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZMPop", reflect.TypeOf((*MockCmdable)(nil).ZMPop), varargs...)
+}
+
+// ZMScore mocks base method.
+func (m *MockCmdable) ZMScore(arg0 context.Context, arg1 string, arg2 ...string) *redis.FloatSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZMScore", varargs...)
+ ret0, _ := ret[0].(*redis.FloatSliceCmd)
+ return ret0
+}
+
+// ZMScore indicates an expected call of ZMScore.
+func (mr *MockCmdableMockRecorder) ZMScore(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZMScore", reflect.TypeOf((*MockCmdable)(nil).ZMScore), varargs...)
+}
+
+// ZPopMax mocks base method.
+func (m *MockCmdable) ZPopMax(arg0 context.Context, arg1 string, arg2 ...int64) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZPopMax", varargs...)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZPopMax indicates an expected call of ZPopMax.
+func (mr *MockCmdableMockRecorder) ZPopMax(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZPopMax", reflect.TypeOf((*MockCmdable)(nil).ZPopMax), varargs...)
+}
+
+// ZPopMin mocks base method.
+func (m *MockCmdable) ZPopMin(arg0 context.Context, arg1 string, arg2 ...int64) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZPopMin", varargs...)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZPopMin indicates an expected call of ZPopMin.
+func (mr *MockCmdableMockRecorder) ZPopMin(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZPopMin", reflect.TypeOf((*MockCmdable)(nil).ZPopMin), varargs...)
+}
+
+// ZRandMember mocks base method.
+func (m *MockCmdable) ZRandMember(arg0 context.Context, arg1 string, arg2 int) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRandMember", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRandMember indicates an expected call of ZRandMember.
+func (mr *MockCmdableMockRecorder) ZRandMember(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRandMember", reflect.TypeOf((*MockCmdable)(nil).ZRandMember), arg0, arg1, arg2)
+}
+
+// ZRandMemberWithScores mocks base method.
+func (m *MockCmdable) ZRandMemberWithScores(arg0 context.Context, arg1 string, arg2 int) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRandMemberWithScores", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRandMemberWithScores indicates an expected call of ZRandMemberWithScores.
+func (mr *MockCmdableMockRecorder) ZRandMemberWithScores(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRandMemberWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRandMemberWithScores), arg0, arg1, arg2)
+}
+
+// ZRange mocks base method.
+func (m *MockCmdable) ZRange(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRange indicates an expected call of ZRange.
+func (mr *MockCmdableMockRecorder) ZRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRange", reflect.TypeOf((*MockCmdable)(nil).ZRange), arg0, arg1, arg2, arg3)
+}
+
+// ZRangeArgs mocks base method.
+func (m *MockCmdable) ZRangeArgs(arg0 context.Context, arg1 redis.ZRangeArgs) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeArgs", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRangeArgs indicates an expected call of ZRangeArgs.
+func (mr *MockCmdableMockRecorder) ZRangeArgs(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeArgs", reflect.TypeOf((*MockCmdable)(nil).ZRangeArgs), arg0, arg1)
+}
+
+// ZRangeArgsWithScores mocks base method.
+func (m *MockCmdable) ZRangeArgsWithScores(arg0 context.Context, arg1 redis.ZRangeArgs) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeArgsWithScores", arg0, arg1)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRangeArgsWithScores indicates an expected call of ZRangeArgsWithScores.
+func (mr *MockCmdableMockRecorder) ZRangeArgsWithScores(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeArgsWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRangeArgsWithScores), arg0, arg1)
+}
+
+// ZRangeByLex mocks base method.
+func (m *MockCmdable) ZRangeByLex(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeByLex", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRangeByLex indicates an expected call of ZRangeByLex.
+func (mr *MockCmdableMockRecorder) ZRangeByLex(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByLex", reflect.TypeOf((*MockCmdable)(nil).ZRangeByLex), arg0, arg1, arg2)
+}
+
+// ZRangeByScore mocks base method.
+func (m *MockCmdable) ZRangeByScore(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeByScore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRangeByScore indicates an expected call of ZRangeByScore.
+func (mr *MockCmdableMockRecorder) ZRangeByScore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByScore", reflect.TypeOf((*MockCmdable)(nil).ZRangeByScore), arg0, arg1, arg2)
+}
+
+// ZRangeByScoreWithScores mocks base method.
+func (m *MockCmdable) ZRangeByScoreWithScores(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeByScoreWithScores", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRangeByScoreWithScores indicates an expected call of ZRangeByScoreWithScores.
+func (mr *MockCmdableMockRecorder) ZRangeByScoreWithScores(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByScoreWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRangeByScoreWithScores), arg0, arg1, arg2)
+}
+
+// ZRangeStore mocks base method.
+func (m *MockCmdable) ZRangeStore(arg0 context.Context, arg1 string, arg2 redis.ZRangeArgs) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeStore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRangeStore indicates an expected call of ZRangeStore.
+func (mr *MockCmdableMockRecorder) ZRangeStore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeStore", reflect.TypeOf((*MockCmdable)(nil).ZRangeStore), arg0, arg1, arg2)
+}
+
+// ZRangeWithScores mocks base method.
+func (m *MockCmdable) ZRangeWithScores(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRangeWithScores", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRangeWithScores indicates an expected call of ZRangeWithScores.
+func (mr *MockCmdableMockRecorder) ZRangeWithScores(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRangeWithScores), arg0, arg1, arg2, arg3)
+}
+
+// ZRank mocks base method.
+func (m *MockCmdable) ZRank(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRank", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRank indicates an expected call of ZRank.
+func (mr *MockCmdableMockRecorder) ZRank(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRank", reflect.TypeOf((*MockCmdable)(nil).ZRank), arg0, arg1, arg2)
+}
+
+// ZRankWithScore mocks base method.
+func (m *MockCmdable) ZRankWithScore(arg0 context.Context, arg1, arg2 string) *redis.RankWithScoreCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRankWithScore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.RankWithScoreCmd)
+ return ret0
+}
+
+// ZRankWithScore indicates an expected call of ZRankWithScore.
+func (mr *MockCmdableMockRecorder) ZRankWithScore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRankWithScore", reflect.TypeOf((*MockCmdable)(nil).ZRankWithScore), arg0, arg1, arg2)
+}
+
+// ZRem mocks base method.
+func (m *MockCmdable) ZRem(arg0 context.Context, arg1 string, arg2 ...interface{}) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ZRem", varargs...)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRem indicates an expected call of ZRem.
+func (mr *MockCmdableMockRecorder) ZRem(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRem", reflect.TypeOf((*MockCmdable)(nil).ZRem), varargs...)
+}
+
+// ZRemRangeByLex mocks base method.
+func (m *MockCmdable) ZRemRangeByLex(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRemRangeByLex", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRemRangeByLex indicates an expected call of ZRemRangeByLex.
+func (mr *MockCmdableMockRecorder) ZRemRangeByLex(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByLex", reflect.TypeOf((*MockCmdable)(nil).ZRemRangeByLex), arg0, arg1, arg2, arg3)
+}
+
+// ZRemRangeByRank mocks base method.
+func (m *MockCmdable) ZRemRangeByRank(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRemRangeByRank", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRemRangeByRank indicates an expected call of ZRemRangeByRank.
+func (mr *MockCmdableMockRecorder) ZRemRangeByRank(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByRank", reflect.TypeOf((*MockCmdable)(nil).ZRemRangeByRank), arg0, arg1, arg2, arg3)
+}
+
+// ZRemRangeByScore mocks base method.
+func (m *MockCmdable) ZRemRangeByScore(arg0 context.Context, arg1, arg2, arg3 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRemRangeByScore", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRemRangeByScore indicates an expected call of ZRemRangeByScore.
+func (mr *MockCmdableMockRecorder) ZRemRangeByScore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByScore", reflect.TypeOf((*MockCmdable)(nil).ZRemRangeByScore), arg0, arg1, arg2, arg3)
+}
+
+// ZRevRange mocks base method.
+func (m *MockCmdable) ZRevRange(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRange", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRevRange indicates an expected call of ZRevRange.
+func (mr *MockCmdableMockRecorder) ZRevRange(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRange", reflect.TypeOf((*MockCmdable)(nil).ZRevRange), arg0, arg1, arg2, arg3)
+}
+
+// ZRevRangeByLex mocks base method.
+func (m *MockCmdable) ZRevRangeByLex(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRangeByLex", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRevRangeByLex indicates an expected call of ZRevRangeByLex.
+func (mr *MockCmdableMockRecorder) ZRevRangeByLex(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByLex", reflect.TypeOf((*MockCmdable)(nil).ZRevRangeByLex), arg0, arg1, arg2)
+}
+
+// ZRevRangeByScore mocks base method.
+func (m *MockCmdable) ZRevRangeByScore(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRangeByScore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZRevRangeByScore indicates an expected call of ZRevRangeByScore.
+func (mr *MockCmdableMockRecorder) ZRevRangeByScore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByScore", reflect.TypeOf((*MockCmdable)(nil).ZRevRangeByScore), arg0, arg1, arg2)
+}
+
+// ZRevRangeByScoreWithScores mocks base method.
+func (m *MockCmdable) ZRevRangeByScoreWithScores(arg0 context.Context, arg1 string, arg2 *redis.ZRangeBy) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRangeByScoreWithScores", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRevRangeByScoreWithScores indicates an expected call of ZRevRangeByScoreWithScores.
+func (mr *MockCmdableMockRecorder) ZRevRangeByScoreWithScores(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByScoreWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRevRangeByScoreWithScores), arg0, arg1, arg2)
+}
+
+// ZRevRangeWithScores mocks base method.
+func (m *MockCmdable) ZRevRangeWithScores(arg0 context.Context, arg1 string, arg2, arg3 int64) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRangeWithScores", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZRevRangeWithScores indicates an expected call of ZRevRangeWithScores.
+func (mr *MockCmdableMockRecorder) ZRevRangeWithScores(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeWithScores", reflect.TypeOf((*MockCmdable)(nil).ZRevRangeWithScores), arg0, arg1, arg2, arg3)
+}
+
+// ZRevRank mocks base method.
+func (m *MockCmdable) ZRevRank(arg0 context.Context, arg1, arg2 string) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRank", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZRevRank indicates an expected call of ZRevRank.
+func (mr *MockCmdableMockRecorder) ZRevRank(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRank", reflect.TypeOf((*MockCmdable)(nil).ZRevRank), arg0, arg1, arg2)
+}
+
+// ZRevRankWithScore mocks base method.
+func (m *MockCmdable) ZRevRankWithScore(arg0 context.Context, arg1, arg2 string) *redis.RankWithScoreCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZRevRankWithScore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.RankWithScoreCmd)
+ return ret0
+}
+
+// ZRevRankWithScore indicates an expected call of ZRevRankWithScore.
+func (mr *MockCmdableMockRecorder) ZRevRankWithScore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRankWithScore", reflect.TypeOf((*MockCmdable)(nil).ZRevRankWithScore), arg0, arg1, arg2)
+}
+
+// ZScan mocks base method.
+func (m *MockCmdable) ZScan(arg0 context.Context, arg1 string, arg2 uint64, arg3 string, arg4 int64) *redis.ScanCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZScan", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*redis.ScanCmd)
+ return ret0
+}
+
+// ZScan indicates an expected call of ZScan.
+func (mr *MockCmdableMockRecorder) ZScan(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZScan", reflect.TypeOf((*MockCmdable)(nil).ZScan), arg0, arg1, arg2, arg3, arg4)
+}
+
+// ZScore mocks base method.
+func (m *MockCmdable) ZScore(arg0 context.Context, arg1, arg2 string) *redis.FloatCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZScore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.FloatCmd)
+ return ret0
+}
+
+// ZScore indicates an expected call of ZScore.
+func (mr *MockCmdableMockRecorder) ZScore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZScore", reflect.TypeOf((*MockCmdable)(nil).ZScore), arg0, arg1, arg2)
+}
+
+// ZUnion mocks base method.
+func (m *MockCmdable) ZUnion(arg0 context.Context, arg1 redis.ZStore) *redis.StringSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZUnion", arg0, arg1)
+ ret0, _ := ret[0].(*redis.StringSliceCmd)
+ return ret0
+}
+
+// ZUnion indicates an expected call of ZUnion.
+func (mr *MockCmdableMockRecorder) ZUnion(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnion", reflect.TypeOf((*MockCmdable)(nil).ZUnion), arg0, arg1)
+}
+
+// ZUnionStore mocks base method.
+func (m *MockCmdable) ZUnionStore(arg0 context.Context, arg1 string, arg2 *redis.ZStore) *redis.IntCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZUnionStore", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*redis.IntCmd)
+ return ret0
+}
+
+// ZUnionStore indicates an expected call of ZUnionStore.
+func (mr *MockCmdableMockRecorder) ZUnionStore(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnionStore", reflect.TypeOf((*MockCmdable)(nil).ZUnionStore), arg0, arg1, arg2)
+}
+
+// ZUnionWithScores mocks base method.
+func (m *MockCmdable) ZUnionWithScores(arg0 context.Context, arg1 redis.ZStore) *redis.ZSliceCmd {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ZUnionWithScores", arg0, arg1)
+ ret0, _ := ret[0].(*redis.ZSliceCmd)
+ return ret0
+}
+
+// ZUnionWithScores indicates an expected call of ZUnionWithScores.
+func (mr *MockCmdableMockRecorder) ZUnionWithScores(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnionWithScores", reflect.TypeOf((*MockCmdable)(nil).ZUnionWithScores), arg0, arg1)
+}
diff --git a/webook/internal/repository/cache/types.go b/webook/internal/repository/cache/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe10043d0fb206919713a8f82556f97a3624d5ca
--- /dev/null
+++ b/webook/internal/repository/cache/types.go
@@ -0,0 +1,34 @@
+package cache
+
+import (
+ "context"
+ "github.com/ecodeclub/ekit"
+ "time"
+)
+
+type Cache interface {
+ Set(ctx context.Context, key string, val any, exp time.Duration) error
+ Get(ctx context.Context, key string) ekit.AnyValue
+}
+
+type LocalCache struct {
+}
+
+type RedisCache struct {
+}
+
+type DoubleCache struct {
+ local Cache
+ redis Cache
+}
+
+func (d *DoubleCache) Set(ctx context.Context,
+ key string, val any, exp time.Duration) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (d *DoubleCache) Get(ctx context.Context, key string) ekit.AnyValue {
+ //TODO implement me
+ panic("implement me")
+}
diff --git a/webook/internal/repository/cache/user.go b/webook/internal/repository/cache/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..9d00a40eef20b6aa832c44c40597ed59a9d01be9
--- /dev/null
+++ b/webook/internal/repository/cache/user.go
@@ -0,0 +1,93 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+var ErrKeyNotExist = redis.Nil
+
+type UserCache interface {
+ Get(ctx context.Context, id int64) (domain.User, error)
+ Set(ctx context.Context, u domain.User) error
+}
+
+type RedisUserCache struct {
+ // 传单机 Redis 可以
+ // 传 cluster 的 Redis 也可以
+ client redis.Cmdable
+ expiration time.Duration
+}
+
+func NewUserCacheV1(addr string) UserCache {
+ client := redis.NewClient(&redis.Options{})
+ return &RedisUserCache{
+ client: client,
+ expiration: time.Minute * 15,
+ }
+}
+
+// NewUserCache
+// A 用到了 B,B 一定是接口 => 这个是保证面向接口
+// A 用到了 B,B 一定是 A 的字段 => 规避包变量、包方法,都非常缺乏扩展性
+// A 用到了 B,A 绝对不初始化 B,而是外面注入 => 保持依赖注入(DI, Dependency Injection)和依赖反转(IOC)
+// expiration 1s, 1m
+func NewUserCache(client redis.Cmdable) UserCache {
+ return &RedisUserCache{
+ client: client,
+ expiration: time.Minute * 15,
+ }
+}
+
+// Get 如果没有数据,返回一个特定的 error
+func (cache *RedisUserCache) Get(ctx context.Context, id int64) (domain.User, error) {
+ //ctx = context.WithValue(ctx, "biz", "user")
+ //ctx = context.WithValue(ctx, "pattern", "user:info:%d")
+ key := cache.key(id)
+ // 数据不存在,err = redis.Nil
+ val, err := cache.client.Get(ctx, key).Bytes()
+ if err != nil {
+ return domain.User{}, err
+ }
+ var u domain.User
+ err = json.Unmarshal(val, &u)
+ //if err != nil {
+ // return domain.User{}, err
+ //}
+ //return u, nil
+ return u, err
+}
+
+func (cache *RedisUserCache) Set(ctx context.Context, u domain.User) error {
+ val, err := json.Marshal(u)
+ if err != nil {
+ return err
+ }
+ key := cache.key(u.Id)
+ return cache.client.Set(ctx, key, val, cache.expiration).Err()
+}
+
+func (cache *RedisUserCache) key(id int64) string {
+ return fmt.Sprintf("user:info:%d", id)
+}
+
+// main 函数里面初始化好
+//var RedisClient *redis.Client
+
+//func GetUser(ctx context.Context, id int64) {
+// RedisClient.Get()
+//}
+
+//type UnifyCache interface {
+// Get(ctx context.Context, firstPageKey string)
+// Set(ctx context.Context, firstPageKey string, val any, expiration time.Duration)
+//}
+//
+//
+//type NewRedisCache() UnifyCache {
+//
+//}
diff --git a/webook/internal/repository/code.go b/webook/internal/repository/code.go
new file mode 100644
index 0000000000000000000000000000000000000000..ce31d318711431ee372fcb0cca5d51e6f5c3f63c
--- /dev/null
+++ b/webook/internal/repository/code.go
@@ -0,0 +1,35 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+)
+
+var (
+ ErrCodeSendTooMany = cache.ErrCodeSendTooMany
+ ErrCodeVerifyTooManyTimes = cache.ErrCodeVerifyTooManyTimes
+)
+
+type CodeRepository interface {
+ Store(ctx context.Context, biz string,
+ phone string, code string) error
+ Verify(ctx context.Context, biz, phone, inputCode string) (bool, error)
+}
+type CachedCodeRepository struct {
+ cache cache.CodeCache
+}
+
+func NewCodeRepository(c cache.CodeCache) CodeRepository {
+ return &CachedCodeRepository{
+ cache: c,
+ }
+}
+
+func (repo *CachedCodeRepository) Store(ctx context.Context, biz string,
+ phone string, code string) error {
+ return repo.cache.Set(ctx, biz, phone, code)
+}
+
+func (repo *CachedCodeRepository) Verify(ctx context.Context, biz, phone, inputCode string) (bool, error) {
+ return repo.cache.Verify(ctx, biz, phone, inputCode)
+}
diff --git a/webook/internal/repository/dao/article/author_dao.go b/webook/internal/repository/dao/article/author_dao.go
new file mode 100644
index 0000000000000000000000000000000000000000..219c5c415b71b14c0fc1ec3a413a45aa4e6c9c01
--- /dev/null
+++ b/webook/internal/repository/dao/article/author_dao.go
@@ -0,0 +1,15 @@
+package article
+
+import (
+ "context"
+ "gorm.io/gorm"
+)
+
+type AuthorDAO interface {
+ Insert(ctx context.Context, art Article) (int64, error)
+ UpdateById(ctx context.Context, article Article) error
+}
+
+func NewAuthorDAO(db *gorm.DB) AuthorDAO {
+ panic("implement me")
+}
diff --git a/webook/internal/repository/dao/article/entity.go b/webook/internal/repository/dao/article/entity.go
new file mode 100644
index 0000000000000000000000000000000000000000..753fe6c8f6629ebdce20f8fc1e38a04c03fcda1a
--- /dev/null
+++ b/webook/internal/repository/dao/article/entity.go
@@ -0,0 +1,72 @@
+package article
+
+type Article struct {
+ //model
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ // 标题的长度
+ // 正常都不会超过这个长度
+ Title string `gorm:"type=varchar(4096)" bson:"title,omitempty"`
+ Content string `gorm:"type=BLOB" bson:"content,omitempty"`
+ // 作者
+ AuthorId int64 `gorm:"index" bson:"author_id,omitempty"`
+ Status uint8 `bson:"status,omitempty"`
+ Ctime int64 `bson:"ctime,omitempty"`
+ Utime int64 `bson:"utime,omitempty"`
+}
+
+// PublishedArticle 衍生类型,偷个懒
+type PublishedArticle Article
+
+// PublishedArticleV1 s3 演示专属
+
+type PublishedArticleV1 struct {
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ Title string `gorm:"type=varchar(4096)" bson:"title,omitempty"`
+ AuthorId int64 `gorm:"index" bson:"author_id,omitempty"`
+ Status uint8 `bson:"status,omitempty"`
+ Ctime int64 `bson:"ctime,omitempty"`
+ Utime int64 `bson:"utime,omitempty"`
+}
+
+//func (u *Article) BeforeCreate(tx *gorm.DB) (err error) {
+// startTime := time.Now()
+// tx.Set("start_time", startTime)
+// slog.Default().Info("这是 BeforeCreate 钩子函数")
+// return nil
+//}
+
+//func (u *Article) AfterCreate(tx *gorm.DB) (err error) {
+// // 我要计算执行时间,我怎么拿到 before 里面的 startTime?
+// val, _ := tx.Get("start_time")
+// startTime, ok := val.(time.Time)
+// if !ok {
+// return nil
+// }
+// // 执行时间就出来了
+// duration := time.Since(startTime)
+// slog.Default().Info("这是 AfterCreate 钩子函数")
+// return nil
+//}
+
+//type model struct {
+//}
+//
+//func (u model) BeforeSave(tx *gorm.DB) (err error) {
+// startTime := time.Now()
+// tx.Set("start_time", startTime)
+// slog.Default().Info("这是 BeforeCreate 钩子函数")
+// return nil
+//}
+
+//func (u model) AfterSave(tx *gorm.DB) (err error) {
+// // 我要计算执行时间,我怎么拿到 before 里面的 startTime?
+// val, _ := tx.Get("start_time")
+// startTime, ok := val.(time.Time)
+// if !ok {
+// return nil
+// }
+// // 执行时间就出来了
+// duration := time.Since(startTime)
+// slog.Default().Info("这是 AfterCreate 钩子函数")
+// return nil
+//}
diff --git a/webook/internal/repository/dao/article/gorm.go b/webook/internal/repository/dao/article/gorm.go
new file mode 100644
index 0000000000000000000000000000000000000000..b70f624861e90f944d6c4fed0befb99df03b2ab8
--- /dev/null
+++ b/webook/internal/repository/dao/article/gorm.go
@@ -0,0 +1,196 @@
+package article
+
+import (
+ "context"
+ "errors"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "time"
+)
+
+type GORMArticleDAO struct {
+ db *gorm.DB
+}
+
+func (dao *GORMArticleDAO) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]Article, error) {
+ var res []Article
+ err := dao.db.WithContext(ctx).
+ Where("utime", start.UnixMilli()).
+ Order("utime DESC").Offset(offset).Limit(limit).Find(&res).Error
+ return res, err
+}
+
+func (dao *GORMArticleDAO) GetByAuthor(ctx context.Context, author int64, offset, limit int) ([]Article, error) {
+ var arts []Article
+ // SELECT * FROM XXX WHERE XX order by aaa
+ // 在设计 order by 语句的时候,要注意让 order by 中的数据命中索引
+ // SQL 优化的案例:早期的时候,
+ // 我们的 order by 没有命中索引的,内存排序非常慢
+ // 你的工作就是优化了这个查询,加进去了索引
+ // author_id => author_id, utime 的联合索引
+ err := dao.db.WithContext(ctx).Model(&Article{}).
+ Where("author_id = ?", author).
+ Offset(offset).
+ Limit(limit).
+ // 升序排序。 utime ASC
+ // 混合排序
+ // ctime ASC, utime desc
+ Order("utime DESC").
+ //Order(clause.OrderBy{Columns: []clause.OrderByColumn{
+ // {Column: clause.Column{Name: "utime"}, Desc: true},
+ // {Column: clause.Column{Name: "ctime"}, Desc: false},
+ //}}).
+ Find(&arts).Error
+ return arts, err
+}
+
+func (dao *GORMArticleDAO) GetPubById(ctx context.Context, id int64) (PublishedArticle, error) {
+ var pub PublishedArticle
+ err := dao.db.WithContext(ctx).
+ Where("id = ?", id).
+ First(&pub).Error
+ return pub, err
+}
+
+func (dao *GORMArticleDAO) GetById(ctx context.Context, id int64) (Article, error) {
+ var art Article
+ err := dao.db.WithContext(ctx).Model(&Article{}).
+ Where("id = ?", id).
+ First(&art).Error
+ return art, err
+}
+
+func NewGORMArticleDAO(db *gorm.DB) ArticleDAO {
+ return &GORMArticleDAO{
+ db: db,
+ }
+}
+
+func (dao *GORMArticleDAO) SyncStatus(ctx context.Context, author, id int64, status uint8) error {
+ return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ res := tx.Model(&Article{}).
+ Where("id=? AND author_id = ?", id, author).
+ Update("status", status)
+ if res.Error != nil {
+ return res.Error
+ }
+ if res.RowsAffected != 1 {
+ return ErrPossibleIncorrectAuthor
+ }
+
+ res = tx.Model(&PublishedArticle{}).
+ Where("id=? AND author_id = ?", id, author).Update("status", status)
+ if res.Error != nil {
+ return res.Error
+ }
+ if res.RowsAffected != 1 {
+ return ErrPossibleIncorrectAuthor
+ }
+ return nil
+ })
+}
+
+func (dao *GORMArticleDAO) Sync(ctx context.Context,
+ art Article) (int64, error) {
+ tx := dao.db.WithContext(ctx).Begin()
+ now := time.Now().UnixMilli()
+ defer tx.Rollback()
+ txDAO := NewGORMArticleDAO(tx)
+ var (
+ id = art.Id
+ err error
+ )
+ if id == 0 {
+ id, err = txDAO.Insert(ctx, art)
+ } else {
+ err = txDAO.UpdateById(ctx, art)
+ }
+ if err != nil {
+ return 0, err
+ }
+ art.Id = id
+ publishArt := PublishedArticle(art)
+ publishArt.Utime = now
+ publishArt.Ctime = now
+ err = tx.Clauses(clause.OnConflict{
+ // ID 冲突的时候。实际上,在 MYSQL 里面你写不写都可以
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.Assignments(map[string]interface{}{
+ "title": art.Title,
+ "content": art.Content,
+ "status": art.Status,
+ "utime": now,
+ }),
+ }).Create(&publishArt).Error
+ if err != nil {
+ return 0, err
+ }
+ tx.Commit()
+ return id, tx.Error
+}
+
+func (dao *GORMArticleDAO) SyncClosure(ctx context.Context,
+ art Article) (int64, error) {
+ var (
+ id = art.Id
+ )
+ err := dao.db.Transaction(func(tx *gorm.DB) error {
+ var err error
+ now := time.Now().UnixMilli()
+ txDAO := NewGORMArticleDAO(tx)
+ if id == 0 {
+ id, err = txDAO.Insert(ctx, art)
+ } else {
+ err = txDAO.UpdateById(ctx, art)
+ }
+ if err != nil {
+ return err
+ }
+ art.Id = id
+ publishArt := art
+ publishArt.Utime = now
+ publishArt.Ctime = now
+ return tx.Clauses(clause.OnConflict{
+ // ID 冲突的时候。实际上,在 MYSQL 里面你写不写都可以
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.Assignments(map[string]interface{}{
+ "title": art.Title,
+ "content": art.Content,
+ "utime": now,
+ }),
+ }).Create(&publishArt).Error
+ })
+ return id, err
+}
+
+func (dao *GORMArticleDAO) Insert(ctx context.Context,
+ art Article) (int64, error) {
+ now := time.Now().UnixMilli()
+ art.Ctime = now
+ art.Utime = now
+ err := dao.db.WithContext(ctx).Create(&art).Error
+ // 返回自增主键
+ return art.Id, err
+}
+
+// UpdateById 只更新标题、内容和状态
+func (dao *GORMArticleDAO) UpdateById(ctx context.Context,
+ art Article) error {
+ now := time.Now().UnixMilli()
+ res := dao.db.Model(&Article{}).WithContext(ctx).
+ Where("id=? AND author_id = ? ", art.Id, art.AuthorId).
+ Updates(map[string]any{
+ "title": art.Title,
+ "content": art.Content,
+ "status": art.Status,
+ "utime": now,
+ })
+ err := res.Error
+ if err != nil {
+ return err
+ }
+ if res.RowsAffected == 0 {
+ return errors.New("更新数据失败")
+ }
+ return nil
+}
diff --git a/webook/internal/repository/dao/article/mongodb.go b/webook/internal/repository/dao/article/mongodb.go
new file mode 100644
index 0000000000000000000000000000000000000000..9aff39c8f386bb78518171f235940a056d94ae0f
--- /dev/null
+++ b/webook/internal/repository/dao/article/mongodb.go
@@ -0,0 +1,181 @@
+package article
+
+import (
+ "context"
+ "errors"
+ "github.com/bwmarrin/snowflake"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "time"
+)
+
+type MongoDBDAO struct {
+ //client *mongo.Client
+ // 代表 webook 的
+ //database *mongo.Database
+ // 代表的是制作库
+ col *mongo.Collection
+ // 代表的是线上库
+ liveCol *mongo.Collection
+ node *snowflake.Node
+
+ idGen IDGenerator
+}
+
+func (m *MongoDBDAO) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]Article, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (m *MongoDBDAO) GetPubById(ctx context.Context, id int64) (PublishedArticle, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (m *MongoDBDAO) GetByAuthor(ctx context.Context, author int64, offset, limit int) ([]Article, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (m *MongoDBDAO) GetById(ctx context.Context, id int64) (Article, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (m *MongoDBDAO) Insert(ctx context.Context, art Article) (int64, error) {
+ now := time.Now().UnixMilli()
+ art.Ctime = now
+ art.Utime = now
+ //id := m.idGen()
+ id := m.node.Generate().Int64()
+ art.Id = id
+ _, err := m.col.InsertOne(ctx, art)
+ // 你没有自增主键
+ // GLOBAL UNIFY ID (GUID,全局唯一ID)
+ return id, err
+}
+
+func (m *MongoDBDAO) UpdateById(ctx context.Context, art Article) error {
+ // 操作制作库
+ filter := bson.M{"id": art.Id, "author_id": art.AuthorId}
+ update := bson.D{bson.E{"$set", bson.M{
+ "title": art.Title,
+ "content": art.Content,
+ "utime": time.Now().UnixMilli(),
+ "status": art.Status,
+ }}}
+ res, err := m.col.UpdateOne(ctx, filter, update)
+ if err != nil {
+ return err
+ }
+
+ // 这边就是校验了 author_id 是不是正确的 ID
+ if res.ModifiedCount == 0 {
+ return errors.New("更新数据失败")
+ }
+ return nil
+}
+
+func (m *MongoDBDAO) Sync(ctx context.Context, art Article) (int64, error) {
+ // 没法子引入事务的概念
+ // 首先第一步,保存制作库
+ var (
+ id = art.Id
+ err error
+ )
+ if id > 0 {
+ err = m.UpdateById(ctx, art)
+ } else {
+ id, err = m.Insert(ctx, art)
+ }
+ if err != nil {
+ return 0, err
+ }
+ art.Id = id
+ // 操作线上库了, upsert 语义
+ now := time.Now().UnixMilli()
+ //update := bson.E{"$set", art}
+ //upsert := bson.E{"$setOnInsert", bson.D{bson.E{"ctime", now}}}
+ art.Utime = now
+ updateV1 := bson.M{
+ // 更新,如果不存在,就是插入,
+ "$set": PublishedArticle(art),
+ // 在插入的时候,要插入 ctime
+ "$setOnInsert": bson.M{"ctime": now},
+ }
+ filter := bson.M{"id": art.Id}
+ _, err = m.liveCol.UpdateOne(ctx, filter,
+ //bson.D{update, upsert},
+ updateV1,
+ options.Update().SetUpsert(true))
+ return id, err
+}
+
+func (m *MongoDBDAO) SyncStatus(ctx context.Context, author, id int64, status uint8) error {
+ panic("implement me")
+}
+
+func InitCollections(db *mongo.Database) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ index := []mongo.IndexModel{
+ {
+ Keys: bson.D{bson.E{Key: "id", Value: 1}},
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{bson.E{Key: "author_id", Value: 1},
+ bson.E{Key: "ctime", Value: 1},
+ },
+ Options: options.Index(),
+ },
+ }
+ _, err := db.Collection("articles").Indexes().
+ CreateMany(ctx, index)
+ if err != nil {
+ return err
+ }
+ _, err = db.Collection("published_articles").Indexes().
+ CreateMany(ctx, index)
+ return err
+}
+
+type IDGenerator func() int64
+
+func NewMongoDBDAOV1(db *mongo.Database, idGen IDGenerator) ArticleDAO {
+ return &MongoDBDAO{
+ col: db.Collection("articles"),
+ liveCol: db.Collection("published_articles"),
+ //node: node,
+ idGen: idGen,
+ }
+}
+
+func NewMongoDBDAO(db *mongo.Database, node *snowflake.Node) ArticleDAO {
+ return &MongoDBDAO{
+ col: db.Collection("articles"),
+ liveCol: db.Collection("published_articles"),
+ node: node,
+ }
+}
+
+//func ToUpdate(vals map[string]any) bson.M {
+// return vals
+//}
+//
+//func ToFilter(vals map[string]any) bson.D {
+// var res bson.D
+// for k, v := range vals {
+// res = append(res, bson.E{k, v})
+// }
+// return res
+//}
+//
+//func Set(vals map[string]any) bson.M {
+// return bson.M{"$set": bson.M(vals)}
+//}
+//
+//func Upset(vals map[string]any) bson.M {
+// return bson.M{"$set": bson.M(vals), "$setOnInsert"}
+//}
diff --git a/webook/internal/repository/dao/article/reader_dao.go b/webook/internal/repository/dao/article/reader_dao.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d8ad837d12ae87a9a2ba2f31640ea0f4da200ec
--- /dev/null
+++ b/webook/internal/repository/dao/article/reader_dao.go
@@ -0,0 +1,15 @@
+package article
+
+import (
+ "context"
+ "gorm.io/gorm"
+)
+
+type ReaderDAO interface {
+ Upsert(ctx context.Context, art Article) error
+ UpsertV2(ctx context.Context, art PublishedArticle) error
+}
+
+func NewReaderDAO(db *gorm.DB) ReaderDAO {
+ panic("implement me")
+}
diff --git a/webook/internal/repository/dao/article/s3.go b/webook/internal/repository/dao/article/s3.go
new file mode 100644
index 0000000000000000000000000000000000000000..b06303e111df4aaf336329a2704aa7cfa5fd41e3
--- /dev/null
+++ b/webook/internal/repository/dao/article/s3.go
@@ -0,0 +1,102 @@
+package article
+
+import (
+ "bytes"
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ _ "github.com/aws/aws-sdk-go"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/ecodeclub/ekit"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "strconv"
+ "time"
+)
+
+var statusPrivate = domain.ArticleStatusPrivate.ToUint8()
+
+type S3DAO struct {
+ oss *s3.S3
+ // 通过组合 GORMArticleDAO 来简化操作
+ // 当然在实践中,你是不太会有组合的机会
+ // 你操作制作库总是一样的
+ // 你就是操作线上库的时候不一样
+ GORMArticleDAO
+ bucket *string
+}
+
+// NewOssDAO 因为组合 GORMArticleDAO 是一个内部实现细节
+// 所以这里要直接传入 DB
+func NewOssDAO(oss *s3.S3, db *gorm.DB) ArticleDAO {
+ return &S3DAO{
+ oss: oss,
+ // 你也可以考虑利用依赖注入来传入。
+ // 但是事实上这个很少变,所以你可以延迟到必要的时候再注入
+ bucket: ekit.ToPtr[string]("webook-1314583317"),
+ GORMArticleDAO: GORMArticleDAO{
+ db: db,
+ },
+ }
+}
+
+func (o *S3DAO) Sync(ctx context.Context, art Article) (int64, error) {
+ // 保存制作库
+ // 保存线上库,并且把 content 上传到 OSS
+ //
+ var (
+ id = art.Id
+ )
+ // 制作库流量不大,并发不高,你就保存到数据库就可以
+ // 当然,有钱或者体量大,就还是考虑 OSS
+ err := o.db.Transaction(func(tx *gorm.DB) error {
+ var err error
+ now := time.Now().UnixMilli()
+ // 制作库
+ txDAO := NewGORMArticleDAO(tx)
+ if id == 0 {
+ id, err = txDAO.Insert(ctx, art)
+ } else {
+ err = txDAO.UpdateById(ctx, art)
+ }
+ if err != nil {
+ return err
+ }
+ art.Id = id
+ publishArt := PublishedArticleV1{
+ Id: art.Id,
+ Title: art.Title,
+ AuthorId: art.AuthorId,
+ Status: art.Status,
+ Ctime: now,
+ Utime: now,
+ }
+ // 线上库不保存 Content,要准备上传到 OSS 里面
+ return tx.Clauses(clause.OnConflict{
+ // ID 冲突的时候。实际上,在 MYSQL 里面你写不写都可以
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.Assignments(map[string]interface{}{
+ "title": art.Title,
+ "utime": now,
+ "status": art.Status,
+ // 要参与 SQL 运算的
+ }),
+ }).Create(&publishArt).Error
+ })
+ // 说明保存到数据库的时候失败了
+ if err != nil {
+ return 0, err
+ }
+ // 接下来就是保存到 OSS 里面
+ // 你要有监控,你要有重试,你要有补偿机制
+ _, err = o.oss.PutObjectWithContext(ctx, &s3.PutObjectInput{
+ Bucket: o.bucket,
+ Key: ekit.ToPtr[string](strconv.FormatInt(art.Id, 10)),
+ Body: bytes.NewReader([]byte(art.Content)),
+ ContentType: ekit.ToPtr[string]("text/plain;charset=utf-8"),
+ })
+ return id, err
+}
+
+func (o *S3DAO) SyncStatus(ctx context.Context, author, id int64, status uint8) error {
+ panic("implement me")
+}
diff --git a/webook/internal/repository/dao/article/s3_test.go b/webook/internal/repository/dao/article/s3_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9dc4219e0ef4cbcd84fda4e8f720b7cbab6ffd36
--- /dev/null
+++ b/webook/internal/repository/dao/article/s3_test.go
@@ -0,0 +1,55 @@
+package article
+
+import (
+ "bytes"
+ "context"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/ecodeclub/ekit"
+ "github.com/stretchr/testify/assert"
+ "io"
+ "os"
+ "testing"
+ "time"
+)
+
+// 你可以用这个来单独测试你的 OSS 配置对不对,有没有权限
+func TestS3(t *testing.T) {
+ // 腾讯云中对标 s3 和 OSS 的产品叫做 COS
+ cosId, ok := os.LookupEnv("COS_APP_ID")
+ if !ok {
+ panic("没有找到环境变量 COS_APP_ID ")
+ }
+ cosKey, ok := os.LookupEnv("COS_APP_SECRET")
+ if !ok {
+ panic("没有找到环境变量 COS_APP_SECRET")
+ }
+ sess, err := session.NewSession(&aws.Config{
+ Credentials: credentials.NewStaticCredentials(cosId, cosKey, ""),
+ Region: ekit.ToPtr[string]("ap-nanjing"),
+ Endpoint: ekit.ToPtr[string]("https://cos.ap-nanjing.myqcloud.com"),
+ // 强制使用 /bucket/key 的形态
+ S3ForcePathStyle: ekit.ToPtr[bool](true),
+ })
+ assert.NoError(t, err)
+ client := s3.New(sess)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+ _, err = client.PutObjectWithContext(ctx, &s3.PutObjectInput{
+ Bucket: ekit.ToPtr[string]("webook-1314583317"),
+ Key: ekit.ToPtr[string]("126"),
+ Body: bytes.NewReader([]byte("测试内容 abc")),
+ ContentType: ekit.ToPtr[string]("text/plain;charset=utf-8"),
+ })
+ assert.NoError(t, err)
+ res, err := client.GetObjectWithContext(ctx, &s3.GetObjectInput{
+ Bucket: ekit.ToPtr[string]("webook-1314583317"),
+ Key: ekit.ToPtr[string]("测试文件"),
+ })
+ assert.NoError(t, err)
+ data, err := io.ReadAll(res.Body)
+ assert.NoError(t, err)
+ t.Log(string(data))
+}
diff --git a/webook/internal/repository/dao/article/types.go b/webook/internal/repository/dao/article/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..4a39703c7793ab0ec410cbf4c244a102d7cc0454
--- /dev/null
+++ b/webook/internal/repository/dao/article/types.go
@@ -0,0 +1,20 @@
+package article
+
+import (
+ "context"
+ "errors"
+ "time"
+)
+
+var ErrPossibleIncorrectAuthor = errors.New("用户在尝试操作非本人数据")
+
+type ArticleDAO interface {
+ Insert(ctx context.Context, art Article) (int64, error)
+ UpdateById(ctx context.Context, art Article) error
+ GetByAuthor(ctx context.Context, author int64, offset, limit int) ([]Article, error)
+ GetById(ctx context.Context, id int64) (Article, error)
+ GetPubById(ctx context.Context, id int64) (PublishedArticle, error)
+ Sync(ctx context.Context, art Article) (int64, error)
+ SyncStatus(ctx context.Context, author, id int64, status uint8) error
+ ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]Article, error)
+}
diff --git a/webook/internal/repository/dao/init.go b/webook/internal/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..383d96ddd0d43dd68bbd5baac50dbc657fb4d913
--- /dev/null
+++ b/webook/internal/repository/dao/init.go
@@ -0,0 +1,14 @@
+package dao
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gorm.io/gorm"
+)
+
+func InitTable(db *gorm.DB) error {
+ return db.AutoMigrate(&User{},
+ &article.Article{},
+ &article.PublishedArticle{},
+ &Job{},
+ )
+}
diff --git a/webook/internal/repository/dao/job.go b/webook/internal/repository/dao/job.go
new file mode 100644
index 0000000000000000000000000000000000000000..265458a2329939cef3c9a12d5d239dac0cdae1b3
--- /dev/null
+++ b/webook/internal/repository/dao/job.go
@@ -0,0 +1,137 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+ "time"
+)
+
+type JobDAO interface {
+ Preempt(ctx context.Context) (Job, error)
+ Release(ctx context.Context, id int64) error
+ UpdateUtime(ctx context.Context, id int64) error
+ UpdateNextTime(ctx context.Context, id int64, next time.Time) error
+ Stop(ctx context.Context, id int64) error
+}
+
+type GORMJobDAO struct {
+ db *gorm.DB
+}
+
+func (g *GORMJobDAO) UpdateUtime(ctx context.Context, id int64) error {
+ return g.db.WithContext(ctx).Model(&Job{}).
+ Where("id =?", id).Updates(map[string]any{
+ "utime": time.Now().UnixMilli(),
+ }).Error
+}
+
+func (g *GORMJobDAO) UpdateNextTime(ctx context.Context, id int64, next time.Time) error {
+ return g.db.WithContext(ctx).Model(&Job{}).
+ Where("id = ?", id).Updates(map[string]any{
+ "next_time": next.UnixMilli(),
+ }).Error
+}
+
+func (g *GORMJobDAO) Stop(ctx context.Context, id int64) error {
+ return g.db.WithContext(ctx).
+ Where("id = ?", id).Updates(map[string]any{
+ "status": jobStatusPaused,
+ "utime": time.Now().UnixMilli(),
+ }).Error
+}
+
+func (g *GORMJobDAO) Release(ctx context.Context, id int64) error {
+ // 这里有一个问题。你要不要检测 status 或者 version?
+ // WHERE version = ?
+ // 要。你们的作业记得修改
+ return g.db.WithContext(ctx).Model(&Job{}).Where("id =?", id).
+ Updates(map[string]any{
+ "status": jobStatusWaiting,
+ "utime": time.Now().UnixMilli(),
+ }).Error
+}
+
+func (g *GORMJobDAO) Preempt(ctx context.Context) (Job, error) {
+ // 高并发情况下,大部分都是陪太子读书
+ // 100 个 goroutine
+ // 要转几次? 所有 goroutine 执行的循环次数加在一起是
+ // 1+2+3+4 +5 + ... + 99 + 100
+ // 特定一个 goroutine,最差情况下,要循环一百次
+ db := g.db.WithContext(ctx)
+ for {
+ now := time.Now()
+ var j Job
+ // 分布式任务调度系统
+ // 1. 一次拉一批,我一次性取出 100 条来,然后,我随机从某一条开始,向后开始抢占
+ // 2. 我搞个随机偏移量,0-100 生成一个随机偏移量。兜底:第一轮没查到,偏移量回归到 0
+ // 3. 我搞一个 id 取余分配,status = ? AND next_time <=? AND id%10 = ? 兜底:不加余数条件,取next_time 最老的
+ err := db.WithContext(ctx).Where("status = ? AND next_time <=?", jobStatusWaiting, now).
+ First(&j).Error
+ // 你找到了,可以被抢占的
+ // 找到之后你要干嘛?你要抢占
+ if err != nil {
+ // // 没有任务。从这里返回
+ return Job{}, err
+ }
+ // 两个 goroutine 都拿到 id =1 的数据
+ // 能不能用 utime?
+ // 乐观锁,CAS 操作,compare AND Swap
+ // 有一个很常见的面试刷亮点:就是用乐观锁取代 FOR UPDATE
+ // 面试套路(性能优化):曾将用了 FOR UPDATE =>性能差,还会有死锁 => 我优化成了乐观锁
+ res := db.Where("id=? AND version = ?",
+ j.Id, j.Version).Model(&Job{}).
+ Updates(map[string]any{
+ "status": jobStatusRunning,
+ "utime": now,
+ "version": j.Version + 1,
+ })
+ if res.Error != nil {
+ return Job{}, err
+ }
+ if res.RowsAffected == 0 {
+ // 抢占失败,你只能说,我要继续下一轮
+ continue
+ }
+ return j, nil
+ }
+}
+
+//
+
+type Job struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ Cfg string
+ Executor string
+ Name string `gorm:"unique"`
+
+ // 第一个问题:哪些任务可以抢?哪些任务已经被人占着?哪些任务永远不会被运行
+ // 用状态来标记
+ Status int
+
+ // 另外一个问题,定时任务,我怎么知道,已经到时间了呢?
+ // NextTime 下一次被调度的时间
+ // next_time <= now 这样一个查询条件
+ // and status = 0
+ // 要建立索引
+ // 更加好的应该是 next_time 和 status 的联合索引
+ NextTime int64 `gorm:"index"`
+ // cron 表达式
+ Cron string
+
+ Version int
+
+ // 创建时间,毫秒数
+ Ctime int64
+ // 更新时间,毫秒数
+ Utime int64
+}
+
+const (
+ jobStatusWaiting = iota
+ // 已经被抢占
+ jobStatusRunning
+ // 还可以有别的取值
+
+ // 暂停调度
+ jobStatusPaused
+)
diff --git a/webook/internal/repository/dao/mocks/user.mock.go b/webook/internal/repository/dao/mocks/user.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..c1ba5e38d84fef5ef65f5836548edc5c8664ecc5
--- /dev/null
+++ b/webook/internal/repository/dao/mocks/user.mock.go
@@ -0,0 +1,110 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/dao/user.go
+
+// Package daomocks is a generated GoMock package.
+package daomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ dao "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockUserDAO is a mock of UserDAO interface.
+type MockUserDAO struct {
+ ctrl *gomock.Controller
+ recorder *MockUserDAOMockRecorder
+}
+
+// MockUserDAOMockRecorder is the mock recorder for MockUserDAO.
+type MockUserDAOMockRecorder struct {
+ mock *MockUserDAO
+}
+
+// NewMockUserDAO creates a new mock instance.
+func NewMockUserDAO(ctrl *gomock.Controller) *MockUserDAO {
+ mock := &MockUserDAO{ctrl: ctrl}
+ mock.recorder = &MockUserDAOMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUserDAO) EXPECT() *MockUserDAOMockRecorder {
+ return m.recorder
+}
+
+// FindByEmail mocks base method.
+func (m *MockUserDAO) FindByEmail(ctx context.Context, email string) (dao.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByEmail", ctx, email)
+ ret0, _ := ret[0].(dao.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByEmail indicates an expected call of FindByEmail.
+func (mr *MockUserDAOMockRecorder) FindByEmail(ctx, email interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockUserDAO)(nil).FindByEmail), ctx, email)
+}
+
+// FindById mocks base method.
+func (m *MockUserDAO) FindById(ctx context.Context, id int64) (dao.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindById", ctx, id)
+ ret0, _ := ret[0].(dao.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindById indicates an expected call of FindById.
+func (mr *MockUserDAOMockRecorder) FindById(ctx, id interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindById", reflect.TypeOf((*MockUserDAO)(nil).FindById), ctx, id)
+}
+
+// FindByPhone mocks base method.
+func (m *MockUserDAO) FindByPhone(ctx context.Context, phone string) (dao.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByPhone", ctx, phone)
+ ret0, _ := ret[0].(dao.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByPhone indicates an expected call of FindByPhone.
+func (mr *MockUserDAOMockRecorder) FindByPhone(ctx, phone interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByPhone", reflect.TypeOf((*MockUserDAO)(nil).FindByPhone), ctx, phone)
+}
+
+// FindByWechat mocks base method.
+func (m *MockUserDAO) FindByWechat(ctx context.Context, openID string) (dao.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByWechat", ctx, openID)
+ ret0, _ := ret[0].(dao.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByWechat indicates an expected call of FindByWechat.
+func (mr *MockUserDAOMockRecorder) FindByWechat(ctx, openID interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByWechat", reflect.TypeOf((*MockUserDAO)(nil).FindByWechat), ctx, openID)
+}
+
+// Insert mocks base method.
+func (m *MockUserDAO) Insert(ctx context.Context, u dao.User) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Insert", ctx, u)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Insert indicates an expected call of Insert.
+func (mr *MockUserDAOMockRecorder) Insert(ctx, u interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockUserDAO)(nil).Insert), ctx, u)
+}
diff --git a/webook/internal/repository/dao/user.go b/webook/internal/repository/dao/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..e12a703f18fe23c8bb22e433220dc24db71ad239
--- /dev/null
+++ b/webook/internal/repository/dao/user.go
@@ -0,0 +1,132 @@
+package dao
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "github.com/go-sql-driver/mysql"
+ "gorm.io/gorm"
+ "time"
+)
+
+var (
+ ErrUserDuplicate = errors.New("邮箱冲突")
+ ErrUserNotFound = gorm.ErrRecordNotFound
+)
+
+type UserDAO interface {
+ FindByEmail(ctx context.Context, email string) (User, error)
+ FindById(ctx context.Context, id int64) (User, error)
+ FindByPhone(ctx context.Context, phone string) (User, error)
+ Insert(ctx context.Context, u User) error
+ FindByWechat(ctx context.Context, openID string) (User, error)
+}
+
+type DBProvider func() *gorm.DB
+
+type GORMUserDAO struct {
+ db *gorm.DB
+
+ p DBProvider
+}
+
+func NewUserDAOV1(p DBProvider) UserDAO {
+ return &GORMUserDAO{
+ p: p,
+ }
+}
+
+func NewUserDAO(db *gorm.DB) UserDAO {
+ res := &GORMUserDAO{
+ db: db,
+ }
+ //viper.OnConfigChange(func(in fsnotify.Event) {
+ // db, err := gorm.Open(mysql.Open())
+ // pt := unsafe.Pointer(&res.db)
+ // atomic.StorePointer(&pt, unsafe.Pointer(&db))
+ //})
+ return res
+}
+
+func (dao *GORMUserDAO) FindByWechat(ctx context.Context, openID string) (User, error) {
+ var u User
+ err := dao.db.WithContext(ctx).Where("wechat_open_id = ?", openID).First(&u).Error
+ //err := dao.p().WithContext(ctx).Where("wechat_open_id = ?", openID).First(&u).Error
+ //err := dao.db.WithContext(ctx).First(&u, "email = ?", email).Error
+ return u, err
+}
+
+func (dao *GORMUserDAO) FindByEmail(ctx context.Context, email string) (User, error) {
+ var u User
+ err := dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
+ //err := dao.db.WithContext(ctx).First(&u, "email = ?", email).Error
+ return u, err
+}
+
+func (dao *GORMUserDAO) FindByPhone(ctx context.Context, phone string) (User, error) {
+ var u User
+ err := dao.db.WithContext(ctx).Where("phone = ?", phone).First(&u).Error
+ //err := dao.db.WithContext(ctx).First(&u, "email = ?", email).Error
+ return u, err
+}
+
+func (dao *GORMUserDAO) FindById(ctx context.Context, id int64) (User, error) {
+ var u User
+ err := dao.db.WithContext(ctx).Where("`id` = ?", id).First(&u).Error
+ return u, err
+}
+
+func (dao *GORMUserDAO) Insert(ctx context.Context, u User) error {
+ // 存毫秒数
+ now := time.Now().UnixMilli()
+ u.Utime = now
+ u.Ctime = now
+ err := dao.db.WithContext(ctx).Create(&u).Error
+ if mysqlErr, ok := err.(*mysql.MySQLError); ok {
+ const uniqueConflictsErrNo uint16 = 1062
+ if mysqlErr.Number == uniqueConflictsErrNo {
+ // 邮箱冲突 or 手机号码冲突
+ return ErrUserDuplicate
+ }
+ }
+ return err
+}
+
+// User 直接对应数据库表结构
+// 有些人叫做 entity,有些人叫做 model,有些人叫做 PO(persistent object)
+type User struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ // 全部用户唯一
+ Email sql.NullString `gorm:"unique"`
+ Password string
+ Nickname string
+
+ // 唯一索引允许有多个空值
+ // 但是不能有多个 ""
+ Phone sql.NullString `gorm:"unique"`
+ // 最大问题就是,你要解引用
+ // 你要判空
+ //Phone *string
+
+ // 往这面加
+
+ // 索引的最左匹配原则:
+ // 假如索引在 建好了
+ // A, AB, ABC 都能用
+ // WHERE A =?
+ // WHERE A = ? AND B =? WHERE B = ? AND A =?
+ // WHERE A = ? AND B = ? AND C = ? ABC 的顺序随便换
+ // WHERE 里面带了 ABC,可以用
+ // WHERE 里面,没有 A,就不能用
+
+ // 如果要创建联合索引,,用 openid 查询的时候不会走索引
+ // 用 unionid 查询的时候,不会走索引
+ // 微信的字段
+ WechatUnionID sql.NullString `gorm:"type=varchar(1024)"`
+ WechatOpenID sql.NullString `gorm:"type=varchar(1024);unique"`
+
+ // 创建时间,毫秒数
+ Ctime int64
+ // 更新时间,毫秒数
+ Utime int64
+}
diff --git a/webook/internal/repository/dao/user_test.go b/webook/internal/repository/dao/user_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f6dc511127dc44cc141b04f8d11364dc2ee4d200
--- /dev/null
+++ b/webook/internal/repository/dao/user_test.go
@@ -0,0 +1,106 @@
+package dao
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "github.com/DATA-DOG/go-sqlmock"
+ "github.com/go-sql-driver/mysql"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ gormMysql "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "testing"
+)
+
+func TestGORMUserDAO_Insert(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ // 为什么不用 ctrl ?
+ // 因为你这里是 sqlmock,不是 gomock
+ mock func(t *testing.T) *sql.DB
+
+ ctx context.Context
+ user User
+
+ wantErr error
+ }{
+ {
+ name: "插入成功",
+ mock: func(t *testing.T) *sql.DB {
+ mockDB, mock, err := sqlmock.New()
+ res := sqlmock.NewResult(3, 1)
+ // 这边预期的是正则表达式
+ // 这个写法的意思就是,只要是 INSERT 到 users 的语句
+ mock.ExpectExec("INSERT INTO `users` .*").
+ WillReturnResult(res)
+ require.NoError(t, err)
+ return mockDB
+ },
+ user: User{
+ Email: sql.NullString{
+ String: "123@qq.com",
+ Valid: true,
+ },
+ },
+ },
+ {
+ name: "邮箱冲突",
+ mock: func(t *testing.T) *sql.DB {
+ mockDB, mock, err := sqlmock.New()
+ // 这边预期的是正则表达式
+ // 这个写法的意思就是,只要是 INSERT 到 users 的语句
+ mock.ExpectExec("INSERT INTO `users` .*").
+ WillReturnError(&mysql.MySQLError{
+ Number: 1062,
+ })
+ require.NoError(t, err)
+ return mockDB
+ },
+ user: User{},
+ wantErr: ErrUserDuplicate,
+ },
+ {
+ name: "数据库错误",
+ mock: func(t *testing.T) *sql.DB {
+ mockDB, mock, err := sqlmock.New()
+ // 这边预期的是正则表达式
+ // 这个写法的意思就是,只要是 INSERT 到 users 的语句
+ mock.ExpectExec("INSERT INTO `users` .*").
+ WillReturnError(errors.New("数据库错误"))
+ require.NoError(t, err)
+ return mockDB
+ },
+ user: User{},
+ wantErr: errors.New("数据库错误"),
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ db, err := gorm.Open(gormMysql.New(gormMysql.Config{
+ Conn: tc.mock(t),
+ // SELECT VERSION;
+ SkipInitializeWithVersion: true,
+ }), &gorm.Config{
+ // 你 mock DB 不需要 ping
+ DisableAutomaticPing: true,
+ // 这个是什么呢?
+ SkipDefaultTransaction: true,
+ })
+ d := NewUserDAO(db)
+ u := tc.user
+ err = d.Insert(tc.ctx, u)
+ assert.Equal(t, tc.wantErr, err)
+ // 你可以比较一下
+ })
+
+ // 理论上让 GORM 执行
+ // INSERT XXX
+
+ // 实际上 GORM
+ // BEGIN;
+ // INSERT
+ // COMMIT;
+ }
+}
diff --git a/webook/internal/repository/history.go b/webook/internal/repository/history.go
new file mode 100644
index 0000000000000000000000000000000000000000..e6a79fab7ba846ccbe82441e6ba1c0363ea34fb1
--- /dev/null
+++ b/webook/internal/repository/history.go
@@ -0,0 +1,7 @@
+package repository
+
+import "context"
+
+type HistoryRecordRepository interface {
+ AddRecord(ctx context.Context)
+}
diff --git a/webook/internal/repository/job.go b/webook/internal/repository/job.go
new file mode 100644
index 0000000000000000000000000000000000000000..db6bbb669ac4189ceddfaeffda34d4cf7264d790
--- /dev/null
+++ b/webook/internal/repository/job.go
@@ -0,0 +1,49 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "time"
+)
+
+type JobRepository interface {
+ Preempt(ctx context.Context) (domain.Job, error)
+ Release(ctx context.Context, id int64) error
+ UpdateUtime(ctx context.Context, id int64) error
+ UpdateNextTime(ctx context.Context, id int64, next time.Time) error
+ Stop(ctx context.Context, id int64) error
+}
+
+type PreemptCronJobRepository struct {
+ dao dao.JobDAO
+}
+
+func (p *PreemptCronJobRepository) UpdateUtime(ctx context.Context, id int64) error {
+ return p.dao.UpdateUtime(ctx, id)
+}
+
+func (p *PreemptCronJobRepository) UpdateNextTime(ctx context.Context, id int64, next time.Time) error {
+ return p.dao.UpdateNextTime(ctx, id, next)
+}
+
+func (p *PreemptCronJobRepository) Stop(ctx context.Context, id int64) error {
+ return p.dao.Stop(ctx, id)
+}
+
+func (p *PreemptCronJobRepository) Release(ctx context.Context, id int64) error {
+ return p.dao.Release(ctx, id)
+}
+
+func (p *PreemptCronJobRepository) Preempt(ctx context.Context) (domain.Job, error) {
+ j, err := p.dao.Preempt(ctx)
+ if err != nil {
+ return domain.Job{}, err
+ }
+ return domain.Job{
+ Cfg: j.Cfg,
+ Id: j.Id,
+ Name: j.Name,
+ Executor: j.Executor,
+ }, nil
+}
diff --git a/webook/internal/repository/mocks/code.mock.go b/webook/internal/repository/mocks/code.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ec0dc23ac617c095ef748c9577dd9d8e70fa949
--- /dev/null
+++ b/webook/internal/repository/mocks/code.mock.go
@@ -0,0 +1,64 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/code.go
+
+// Package repomocks is a generated GoMock package.
+package repomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockCodeRepository is a mock of CodeRepository interface.
+type MockCodeRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockCodeRepositoryMockRecorder
+}
+
+// MockCodeRepositoryMockRecorder is the mock recorder for MockCodeRepository.
+type MockCodeRepositoryMockRecorder struct {
+ mock *MockCodeRepository
+}
+
+// NewMockCodeRepository creates a new mock instance.
+func NewMockCodeRepository(ctrl *gomock.Controller) *MockCodeRepository {
+ mock := &MockCodeRepository{ctrl: ctrl}
+ mock.recorder = &MockCodeRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCodeRepository) EXPECT() *MockCodeRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Store mocks base method.
+func (m *MockCodeRepository) Store(ctx context.Context, biz, phone, code string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Store", ctx, biz, phone, code)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Store indicates an expected call of Store.
+func (mr *MockCodeRepositoryMockRecorder) Store(ctx, biz, phone, code interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockCodeRepository)(nil).Store), ctx, biz, phone, code)
+}
+
+// Verify mocks base method.
+func (m *MockCodeRepository) Verify(ctx context.Context, biz, phone, inputCode string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Verify", ctx, biz, phone, inputCode)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Verify indicates an expected call of Verify.
+func (mr *MockCodeRepositoryMockRecorder) Verify(ctx, biz, phone, inputCode interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockCodeRepository)(nil).Verify), ctx, biz, phone, inputCode)
+}
diff --git a/webook/internal/repository/mocks/cron_job.mock.go b/webook/internal/repository/mocks/cron_job.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad627c81878dc817d69e76de23e1c95d76d2e597
--- /dev/null
+++ b/webook/internal/repository/mocks/cron_job.mock.go
@@ -0,0 +1,98 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./cron_job.go
+//
+// Generated by this command:
+//
+// mockgen -source=./cron_job.go -package=repomocks -destination=mocks/cron_job.mock.go CronJobRepository
+//
+// Package repomocks is a generated GoMock package.
+package repomocks
+
+import (
+ context "context"
+ reflect "reflect"
+ time "time"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockCronJobRepository is a mock of CronJobRepository interface.
+type MockCronJobRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockCronJobRepositoryMockRecorder
+}
+
+// MockCronJobRepositoryMockRecorder is the mock recorder for MockCronJobRepository.
+type MockCronJobRepositoryMockRecorder struct {
+ mock *MockCronJobRepository
+}
+
+// NewMockCronJobRepository creates a new mock instance.
+func NewMockCronJobRepository(ctrl *gomock.Controller) *MockCronJobRepository {
+ mock := &MockCronJobRepository{ctrl: ctrl}
+ mock.recorder = &MockCronJobRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCronJobRepository) EXPECT() *MockCronJobRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Preempt mocks base method.
+func (m *MockCronJobRepository) Preempt(ctx context.Context) (domain.CronJob, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Preempt", ctx)
+ ret0, _ := ret[0].(domain.CronJob)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Preempt indicates an expected call of Preempt.
+func (mr *MockCronJobRepositoryMockRecorder) Preempt(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Preempt", reflect.TypeOf((*MockCronJobRepository)(nil).Preempt), ctx)
+}
+
+// Release mocks base method.
+func (m *MockCronJobRepository) Release(ctx context.Context, id int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Release", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Release indicates an expected call of Release.
+func (mr *MockCronJobRepositoryMockRecorder) Release(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockCronJobRepository)(nil).Release), ctx, id)
+}
+
+// UpdateNextTime mocks base method.
+func (m *MockCronJobRepository) UpdateNextTime(ctx context.Context, id int64, t time.Time) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateNextTime", ctx, id, t)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateNextTime indicates an expected call of UpdateNextTime.
+func (mr *MockCronJobRepositoryMockRecorder) UpdateNextTime(ctx, id, t any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNextTime", reflect.TypeOf((*MockCronJobRepository)(nil).UpdateNextTime), ctx, id, t)
+}
+
+// UpdateUtime mocks base method.
+func (m *MockCronJobRepository) UpdateUtime(ctx context.Context, id int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateUtime", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateUtime indicates an expected call of UpdateUtime.
+func (mr *MockCronJobRepositoryMockRecorder) UpdateUtime(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUtime", reflect.TypeOf((*MockCronJobRepository)(nil).UpdateUtime), ctx, id)
+}
diff --git a/webook/internal/repository/mocks/user.mock.go b/webook/internal/repository/mocks/user.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..39d24bdb4fd9ac752a03f62e73f9d28c96827cc6
--- /dev/null
+++ b/webook/internal/repository/mocks/user.mock.go
@@ -0,0 +1,110 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/repository/user.go
+
+// Package repomocks is a generated GoMock package.
+package repomocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockUserRepository is a mock of UserRepository interface.
+type MockUserRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockUserRepositoryMockRecorder
+}
+
+// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository.
+type MockUserRepositoryMockRecorder struct {
+ mock *MockUserRepository
+}
+
+// NewMockUserRepository creates a new mock instance.
+func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
+ mock := &MockUserRepository{ctrl: ctrl}
+ mock.recorder = &MockUserRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
+ return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockUserRepository) Create(ctx context.Context, u domain.User) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Create", ctx, u)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockUserRepositoryMockRecorder) Create(ctx, u interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserRepository)(nil).Create), ctx, u)
+}
+
+// FindByEmail mocks base method.
+func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByEmail", ctx, email)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByEmail indicates an expected call of FindByEmail.
+func (mr *MockUserRepositoryMockRecorder) FindByEmail(ctx, email interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockUserRepository)(nil).FindByEmail), ctx, email)
+}
+
+// FindById mocks base method.
+func (m *MockUserRepository) FindById(ctx context.Context, id int64) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindById", ctx, id)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindById indicates an expected call of FindById.
+func (mr *MockUserRepositoryMockRecorder) FindById(ctx, id interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindById", reflect.TypeOf((*MockUserRepository)(nil).FindById), ctx, id)
+}
+
+// FindByPhone mocks base method.
+func (m *MockUserRepository) FindByPhone(ctx context.Context, phone string) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByPhone", ctx, phone)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByPhone indicates an expected call of FindByPhone.
+func (mr *MockUserRepositoryMockRecorder) FindByPhone(ctx, phone interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByPhone", reflect.TypeOf((*MockUserRepository)(nil).FindByPhone), ctx, phone)
+}
+
+// FindByWechat mocks base method.
+func (m *MockUserRepository) FindByWechat(ctx context.Context, openID string) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindByWechat", ctx, openID)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindByWechat indicates an expected call of FindByWechat.
+func (mr *MockUserRepositoryMockRecorder) FindByWechat(ctx, openID interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByWechat", reflect.TypeOf((*MockUserRepository)(nil).FindByWechat), ctx, openID)
+}
diff --git a/webook/internal/repository/ranking.go b/webook/internal/repository/ranking.go
new file mode 100644
index 0000000000000000000000000000000000000000..53bbda3228fe2d880ee94d57b1e3527ca3b98bcc
--- /dev/null
+++ b/webook/internal/repository/ranking.go
@@ -0,0 +1,44 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+)
+
+type RankingRepository interface {
+ ReplaceTopN(ctx context.Context, arts []domain.Article) error
+ GetTopN(ctx context.Context) ([]domain.Article, error)
+}
+
+type CachedRankingRepository struct {
+ // 使用具体实现,可读性更好,对测试不友好,因为咩有面向接口编程
+ redis *cache.RankingRedisCache
+ local *cache.RankingLocalCache
+}
+
+func (c *CachedRankingRepository) GetTopN(ctx context.Context) ([]domain.Article, error) {
+ data, err := c.local.Get(ctx)
+ if err == nil {
+ return data, nil
+ }
+ data, err = c.redis.Get(ctx)
+ if err == nil {
+ c.local.Set(ctx, data)
+ } else {
+ return c.local.ForceGet(ctx)
+ }
+ return data, err
+}
+
+func NewCachedRankingRepository(
+ redis *cache.RankingRedisCache,
+ local *cache.RankingLocalCache,
+) RankingRepository {
+ return &CachedRankingRepository{local: local, redis: redis}
+}
+
+func (c *CachedRankingRepository) ReplaceTopN(ctx context.Context, arts []domain.Article) error {
+ _ = c.local.Set(ctx, arts)
+ return c.redis.Set(ctx, arts)
+}
diff --git a/webook/internal/repository/user.go b/webook/internal/repository/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7070c1fc82f8d55b877ba7b512550c7f2872e8c
--- /dev/null
+++ b/webook/internal/repository/user.go
@@ -0,0 +1,159 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "time"
+)
+
+var (
+ ErrUserDuplicate = dao.ErrUserDuplicate
+ ErrUserNotFound = dao.ErrUserNotFound
+)
+
+// UserRepository 是核心,它有不同实现。
+// 但是 Factory 本身如果只是初始化一下,那么它不是你的核心
+type UserRepository interface {
+ FindByEmail(ctx context.Context, email string) (domain.User, error)
+ FindByPhone(ctx context.Context, phone string) (domain.User, error)
+ Create(ctx context.Context, u domain.User) error
+ FindById(ctx context.Context, id int64) (domain.User, error)
+ FindByWechat(ctx context.Context, openID string) (domain.User, error)
+}
+
+type CachedUserRepository struct {
+ dao dao.UserDAO
+ cache cache.UserCache
+ //testSignal chan struct{}
+}
+
+func NewUserRepository(dao dao.UserDAO, c cache.UserCache) UserRepository {
+ return &CachedUserRepository{
+ dao: dao,
+ cache: c,
+ }
+}
+
+func (r *CachedUserRepository) FindByWechat(ctx context.Context, openID string) (domain.User, error) {
+ u, err := r.dao.FindByWechat(ctx, openID)
+ if err != nil {
+ return domain.User{}, err
+ }
+ return r.entityToDomain(u), nil
+}
+
+func (r *CachedUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
+ // SELECT * FROM `users` WHERE `email`=?
+ u, err := r.dao.FindByEmail(ctx, email)
+ if err != nil {
+ return domain.User{}, err
+ }
+ return r.entityToDomain(u), nil
+}
+
+func (r *CachedUserRepository) FindByPhone(ctx context.Context, phone string) (domain.User, error) {
+ u, err := r.dao.FindByPhone(ctx, phone)
+ if err != nil {
+ return domain.User{}, err
+ }
+ return r.entityToDomain(u), nil
+}
+
+func (r *CachedUserRepository) Create(ctx context.Context, u domain.User) error {
+ return r.dao.Insert(ctx, r.domainToEntity(u))
+}
+
+func (r *CachedUserRepository) FindById(ctx context.Context, id int64) (domain.User, error) {
+ u, err := r.cache.Get(ctx, id)
+ if err == nil {
+ // 必然是有数据
+ return u, nil
+ }
+ // 没这个数据
+ //if err == cache.ErrKeyNotExist {
+ // 去数据库里面加载
+ //}
+
+ // 尝试去数据库查询
+ if ctx.Value("limited") == "true" {
+ // 不去了
+ return domain.User{}, errors.New("触发限流,缓存未命中,不查询数据库")
+ }
+
+ ue, err := r.dao.FindById(ctx, id)
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ u = r.entityToDomain(ue)
+
+ //if err != nil {
+ // 我这里怎么办?
+ // 打日志,做监控
+ //return domain.User{}, err
+ //}
+
+ go func() {
+ _ = r.cache.Set(ctx, u)
+ //r.testSignal <- struct{}{}
+ }()
+ return u, nil
+
+ // 这里怎么办? err = io.EOF
+ // 要不要去数据库加载?
+ // 看起来我不应该加载?
+ // 看起来我好像也要加载?
+
+ // 选加载 —— 做好兜底,万一 Redis 真的崩了,你要保护住你的数据库
+ // 我数据库限流呀!
+
+ // 选不加载 —— 用户体验差一点
+
+ // 缓存里面有数据
+ // 缓存里面没有数据
+ // 缓存出错了,你也不知道有没有数据
+
+}
+
+func (r *CachedUserRepository) domainToEntity(u domain.User) dao.User {
+ return dao.User{
+ Id: u.Id,
+ Email: sql.NullString{
+ String: u.Email,
+ // 我确实有手机号
+ Valid: u.Email != "",
+ },
+ Phone: sql.NullString{
+ String: u.Phone,
+ Valid: u.Phone != "",
+ },
+ Password: u.Password,
+ WechatOpenID: sql.NullString{
+ String: u.WechatInfo.OpenID,
+ Valid: u.WechatInfo.OpenID != "",
+ },
+ WechatUnionID: sql.NullString{
+ String: u.WechatInfo.UnionID,
+ Valid: u.WechatInfo.UnionID != "",
+ },
+ Ctime: u.Ctime.UnixMilli(),
+ }
+}
+
+func (r *CachedUserRepository) entityToDomain(u dao.User) domain.User {
+ return domain.User{
+ Id: u.Id,
+ Email: u.Email.String,
+ Password: u.Password,
+ Phone: u.Phone.String,
+ WechatInfo: domain.WechatInfo{
+ UnionID: u.WechatUnionID.String,
+ OpenID: u.WechatOpenID.String,
+ },
+ Ctime: time.UnixMilli(u.Ctime),
+ }
+}
diff --git a/webook/internal/repository/user_test.go b/webook/internal/repository/user_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..44478be0d5e6be17f381c3a77a066355286f7b2e
--- /dev/null
+++ b/webook/internal/repository/user_test.go
@@ -0,0 +1,142 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ cachemocks "gitee.com/geekbang/basic-go/webook/internal/repository/cache/mocks"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ daomocks "gitee.com/geekbang/basic-go/webook/internal/repository/dao/mocks"
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "testing"
+ "time"
+)
+
+func TestCachedUserRepository_FindById(t *testing.T) {
+ // 111ms.11111ns
+ now := time.Now()
+ // 你要去掉毫秒以外的部分
+ // 111ms
+ now = time.UnixMilli(now.UnixMilli())
+ testCases := []struct {
+ name string
+
+ mock func(ctrl *gomock.Controller) (dao.UserDAO, cache.UserCache)
+
+ ctx context.Context
+ id int64
+
+ wantUser domain.User
+ wantErr error
+ }{
+ {
+ name: "缓存未命中,查询成功",
+ mock: func(ctrl *gomock.Controller) (dao.UserDAO, cache.UserCache) {
+ // 缓存未命中,查了缓存,但是没结果
+ c := cachemocks.NewMockUserCache(ctrl)
+ c.EXPECT().Get(gomock.Any(), int64(123)).
+ Return(domain.User{}, cache.ErrKeyNotExist)
+
+ d := daomocks.NewMockUserDAO(ctrl)
+ d.EXPECT().FindById(gomock.Any(), int64(123)).
+ Return(dao.User{
+ Id: 123,
+ Email: sql.NullString{
+ String: "123@qq.com",
+ Valid: true,
+ },
+ Password: "this is password",
+ Phone: sql.NullString{
+ String: "15212345678",
+ Valid: true,
+ },
+ Ctime: now.UnixMilli(),
+ Utime: now.UnixMilli(),
+ }, nil)
+
+ c.EXPECT().Set(gomock.Any(), domain.User{
+ Id: 123,
+ Email: "123@qq.com",
+ Password: "this is password",
+ Phone: "15212345678",
+ Ctime: now,
+ }).Return(nil)
+ return d, c
+ },
+
+ ctx: context.Background(),
+ id: 123,
+ wantUser: domain.User{
+ Id: 123,
+ Email: "123@qq.com",
+ Password: "this is password",
+ Phone: "15212345678",
+ Ctime: now,
+ },
+ wantErr: nil,
+ },
+ {
+ name: "缓存命中",
+ mock: func(ctrl *gomock.Controller) (dao.UserDAO, cache.UserCache) {
+ // 缓存未命中,查了缓存,但是没结果
+ c := cachemocks.NewMockUserCache(ctrl)
+ c.EXPECT().Get(gomock.Any(), int64(123)).
+ Return(domain.User{
+ Id: 123,
+ Email: "123@qq.com",
+ Password: "this is password",
+ Phone: "15212345678",
+ Ctime: now,
+ }, nil)
+ d := daomocks.NewMockUserDAO(ctrl)
+ return d, c
+ },
+
+ ctx: context.Background(),
+ id: 123,
+ wantUser: domain.User{
+ Id: 123,
+ Email: "123@qq.com",
+ Password: "this is password",
+ Phone: "15212345678",
+ Ctime: now,
+ },
+ wantErr: nil,
+ },
+ {
+ name: "缓存未命中,查询失败",
+ mock: func(ctrl *gomock.Controller) (dao.UserDAO, cache.UserCache) {
+ // 缓存未命中,查了缓存,但是没结果
+ c := cachemocks.NewMockUserCache(ctrl)
+ c.EXPECT().Get(gomock.Any(), int64(123)).
+ Return(domain.User{}, cache.ErrKeyNotExist)
+
+ d := daomocks.NewMockUserDAO(ctrl)
+ d.EXPECT().FindById(gomock.Any(), int64(123)).
+ Return(dao.User{}, errors.New("mock db 错误"))
+ return d, c
+ },
+
+ ctx: context.Background(),
+ id: 123,
+ wantUser: domain.User{},
+ wantErr: errors.New("mock db 错误"),
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ ud, uc := tc.mock(ctrl)
+ repo := NewUserRepository(ud, uc)
+ u, err := repo.FindById(tc.ctx, tc.id)
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantUser, u)
+ time.Sleep(time.Second)
+ // 检测 testSignal
+ })
+ }
+}
diff --git a/webook/internal/service/article.go b/webook/internal/service/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..5317b1c4df6f2909dbb67cd1a6a81d3d18eb8a6a
--- /dev/null
+++ b/webook/internal/service/article.go
@@ -0,0 +1,299 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ events "gitee.com/geekbang/basic-go/webook/internal/events/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "time"
+)
+
+//go:generate mockgen -source=article.go -package=svcmocks -destination=mocks/article.mock.go ArticleService
+type ArticleService interface {
+ Save(ctx context.Context, art domain.Article) (int64, error)
+ Withdraw(ctx context.Context, art domain.Article) error
+ Publish(ctx context.Context, art domain.Article) (int64, error)
+ PublishV1(ctx context.Context, art domain.Article) (int64, error)
+ List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error)
+ // ListPub 根据这个 start 时间来查询
+ ListPub(ctx context.Context, start time.Time, offset, limit int) ([]domain.Article, error)
+ GetById(ctx context.Context, id int64) (domain.Article, error)
+ GetPublishedById(ctx context.Context, id, uid int64) (domain.Article, error)
+}
+
+type articleService struct {
+ repo article.ArticleRepository
+
+ // V1 依靠两个不同的 repository 来解决这种跨表,或者跨库的问题
+ author article.ArticleAuthorRepository
+ reader article.ArticleReaderRepository
+ l logger.LoggerV1
+ producer events.Producer
+
+ ch chan readInfo
+}
+
+func (svc *articleService) ListPub(ctx context.Context,
+ start time.Time, offset, limit int) ([]domain.Article, error) {
+ return svc.repo.ListPub(ctx, start, offset, limit)
+}
+
+type readInfo struct {
+ uid int64
+ aid int64
+}
+
+// GetPublishedByIdV1 批量发送的例子
+func (svc *articleService) GetPublishedByIdV1(ctx context.Context, id, uid int64) (domain.Article, error) {
+ // 另一个选项,在这里组装 Author,调用 UserService
+ art, err := svc.repo.GetPublishedById(ctx, id)
+ if err == nil {
+ go func() {
+ // 改批量的做法
+ svc.ch <- readInfo{
+ aid: id,
+ uid: uid,
+ }
+ }()
+ }
+ return art, err
+}
+
+func (svc *articleService) batchSendReadInfo(ctx context.Context) {
+ // 10 个一批
+ // 单个转批量都要考虑的兜底问题
+ for {
+ ctx, cancel := context.WithTimeout(ctx, time.Second)
+ const batchSize = 10
+ uids := make([]int64, 0, 10)
+ aids := make([]int64, 0, 10)
+ send := false
+ for !send {
+ select {
+ // 这边是超时了
+ case <-ctx.Done():
+ // 也要执行发送
+ //goto send
+ send = true
+ case info, ok := <-svc.ch:
+ if !ok {
+ cancel()
+ send = true
+ continue
+ }
+ uids = append(uids, info.uid)
+ aids = append(aids, info.aid)
+ // 凑够了
+ if len(uids) == batchSize {
+ //goto send
+ send = true
+ }
+ }
+ }
+ //send:
+ // 装满了,凑够了一批
+ svc.producer.ProduceReadEventV1(context.Background(),
+ events.ReadEventV1{
+ Uids: uids,
+ Aids: aids,
+ })
+ cancel()
+ }
+}
+
+func (svc *articleService) GetPublishedById(ctx context.Context, id, uid int64) (domain.Article, error) {
+ // 另一个选项,在这里组装 Author,调用 UserService
+ art, err := svc.repo.GetPublishedById(ctx, id)
+ if err == nil {
+ // 每次打开一篇文章,就发一条消息
+ go func() {
+ // 生产者也可以通过改批量来提高性能
+ er := svc.producer.ProduceReadEvent(
+ ctx,
+ events.ReadEvent{
+ // 即便你的消费者要用 art 的里面的数据,
+ // 让它去查询,你不要在 event 里面带
+ Uid: uid,
+ Aid: id,
+ })
+ if er != nil {
+ svc.l.Error("发送读者阅读事件失败")
+ }
+ }()
+
+ //go func() {
+ // // 改批量的做法
+ // svc.ch <- readInfo{
+ // aid: id,
+ // uid: uid,
+ // }
+ //}()
+ }
+ return art, err
+}
+
+func (a *articleService) GetById(ctx context.Context, id int64) (domain.Article, error) {
+ return a.repo.GetByID(ctx, id)
+}
+
+func (a *articleService) List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error) {
+ return a.repo.List(ctx, uid, offset, limit)
+}
+
+func (a *articleService) Withdraw(ctx context.Context, art domain.Article) error {
+ // art.Status = domain.ArticleStatusPrivate 然后直接把整个 art 往下传
+ return a.repo.SyncStatus(ctx, art.Id, art.Author.Id, domain.ArticleStatusPrivate)
+}
+
+func (a *articleService) Publish(ctx context.Context, art domain.Article) (int64, error) {
+ art.Status = domain.ArticleStatusPublished
+ // 制作库
+ //id, err := a.repo.Create(ctx, art)
+ //// 线上库呢?
+ //a.repo.SyncToLiveDB(ctx, art)
+ return a.repo.Sync(ctx, art)
+}
+
+func (a *articleService) PublishV1(ctx context.Context, art domain.Article) (int64, error) {
+ var (
+ id = art.Id
+ err error
+ )
+ if art.Id > 0 {
+ err = a.author.Update(ctx, art)
+ } else {
+ id, err = a.author.Create(ctx, art)
+ }
+ if err != nil {
+ return 0, err
+ }
+ art.Id = id
+ for i := 0; i < 3; i++ {
+ time.Sleep(time.Second * time.Duration(i))
+ id, err = a.reader.Save(ctx, art)
+ if err == nil {
+ break
+ }
+ a.l.Error("部分失败,保存到线上库失败",
+ logger.Int64("art_id", art.Id),
+ logger.Error(err))
+ }
+ if err != nil {
+ a.l.Error("部分失败,重试彻底失败",
+ logger.Int64("art_id", art.Id),
+ logger.Error(err))
+ // 接入你的告警系统,手工处理一下
+ // 走异步,我直接保存到本地文件
+ // 走 Canal
+ // 打 MQ
+ }
+ return id, err
+}
+
+func NewArticleService(repo article.ArticleRepository,
+ l logger.LoggerV1,
+ producer events.Producer) ArticleService {
+ res := &articleService{
+ repo: repo,
+ producer: producer,
+ l: l,
+ //ch: make(chan readInfo, 10),
+ }
+ return res
+}
+
+// ctx, cancel := context.WithCancel(context.Background())
+// NewArticleServiceV3(ctx)
+// 这里一大堆业务逻辑
+// 主程序(main 函数准备退出)
+// cancel()
+func NewArticleServiceV3(ctx context.Context, repo article.ArticleRepository,
+ l logger.LoggerV1,
+ producer events.Producer) ArticleService {
+ res := &articleService{
+ repo: repo,
+ producer: producer,
+ l: l,
+ //ch: make(chan readInfo, 10),
+ }
+ go func() {
+ // 我系统关闭的时候,你 channel 里面还有数据,没发出去,怎么办?
+ // 第一种是啥也不干,你在关闭的时候,time.Sleep
+ // 第二种
+ res.batchSendReadInfo(ctx)
+ }()
+ return res
+}
+
+func (a *articleService) Close() error {
+ close(a.ch)
+ return nil
+}
+
+func NewArticleServiceV2(repo article.ArticleRepository,
+ l logger.LoggerV1,
+ producer events.Producer) ArticleService {
+ ch := make(chan readInfo, 10)
+ go func() {
+ for {
+ uids := make([]int64, 0, 10)
+ aids := make([]int64, 0, 10)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ for i := 0; i < 10; i++ {
+ select {
+ case info, ok := <-ch:
+ if !ok {
+ cancel()
+ return
+ }
+ uids = append(uids, info.uid)
+ aids = append(aids, info.aid)
+ case <-ctx.Done():
+ break
+ }
+ }
+ cancel()
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second)
+ producer.ProduceReadEventV1(ctx, events.ReadEventV1{
+ Uids: uids,
+ Aids: aids,
+ })
+ cancel()
+ }
+ }()
+ return &articleService{
+ repo: repo,
+ producer: producer,
+ l: l,
+ ch: ch,
+ }
+}
+
+func NewArticleServiceV1(author article.ArticleAuthorRepository,
+ reader article.ArticleReaderRepository, l logger.LoggerV1) ArticleService {
+ return &articleService{
+ author: author,
+ reader: reader,
+ l: l,
+ }
+}
+
+func (a *articleService) Save(ctx context.Context, art domain.Article) (int64, error) {
+ art.Status = domain.ArticleStatusUnpublished
+ if art.Id > 0 {
+ err := a.repo.Update(ctx, art)
+ return art.Id, err
+ }
+ return a.repo.Create(ctx, art)
+}
+
+func (a *articleService) update(ctx context.Context, art domain.Article) error {
+ // 只要你不更新 author_id
+ // 但是性能比较差
+ //artInDB := a.repo.FindById(ctx, art.Id)
+ //if art.Author.Id != artInDB.Author.Id {
+ // return errors.New("更新别人的数据")
+ //}
+ return a.repo.Update(ctx, art)
+}
diff --git a/webook/internal/service/article_test.go b/webook/internal/service/article_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..0d8546285f7d3aabf8bf310db572471d9136a75c
--- /dev/null
+++ b/webook/internal/service/article_test.go
@@ -0,0 +1,219 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ artrepomocks "gitee.com/geekbang/basic-go/webook/internal/repository/article/mocks"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "testing"
+)
+
+func Test_articleService_Publish(t *testing.T) {
+ testCases := []struct {
+ name string
+ mock func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository)
+
+ art domain.Article
+
+ wantErr error
+ wantId int64
+ }{
+ {
+ name: "新建发表成功",
+ mock: func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository) {
+ author := artrepomocks.NewMockArticleAuthorRepository(ctrl)
+ author.EXPECT().Create(gomock.Any(), domain.Article{
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(1), nil)
+ reader := artrepomocks.NewMockArticleReaderRepository(ctrl)
+ reader.EXPECT().Save(gomock.Any(), domain.Article{
+ // 确保使用了制作库 ID
+ Id: 1,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(1), nil)
+ return author, reader
+ },
+ art: domain.Article{
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ },
+ wantId: 1,
+ },
+ {
+ name: "修改并发表成功",
+ mock: func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository) {
+ author := artrepomocks.NewMockArticleAuthorRepository(ctrl)
+ author.EXPECT().Update(gomock.Any(), domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(nil)
+ reader := artrepomocks.NewMockArticleReaderRepository(ctrl)
+ reader.EXPECT().Save(gomock.Any(), domain.Article{
+ // 确保使用了制作库 ID
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(2), nil)
+ return author, reader
+ },
+ art: domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ },
+ wantId: 2,
+ },
+ {
+ // 新建-保存到制作库失败
+ // 修改-保存到制作库失败
+ name: "保存到制作库失败",
+ mock: func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository) {
+ author := artrepomocks.NewMockArticleAuthorRepository(ctrl)
+ author.EXPECT().Update(gomock.Any(), domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(errors.New("mock db error"))
+ reader := artrepomocks.NewMockArticleReaderRepository(ctrl)
+ return author, reader
+ },
+ art: domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ },
+ wantId: 0,
+ wantErr: errors.New("mock db error"),
+ },
+ {
+ // 部分失败
+ name: "保存到制作库成功,重试到线上库成功",
+ mock: func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository) {
+ author := artrepomocks.NewMockArticleAuthorRepository(ctrl)
+ author.EXPECT().Update(gomock.Any(), domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(nil)
+ reader := artrepomocks.NewMockArticleReaderRepository(ctrl)
+ reader.EXPECT().Save(gomock.Any(), domain.Article{
+ // 确保使用了制作库 ID
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(0), errors.New("mock db error"))
+ reader.EXPECT().Save(gomock.Any(), domain.Article{
+ // 确保使用了制作库 ID
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(2), nil)
+ return author, reader
+ },
+ art: domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ },
+ wantId: 2,
+ wantErr: nil,
+ },
+ {
+ // 部分失败
+ name: "保存到制作库成功,重试全部失败",
+ mock: func(ctrl *gomock.Controller) (article.ArticleAuthorRepository,
+ article.ArticleReaderRepository) {
+ author := artrepomocks.NewMockArticleAuthorRepository(ctrl)
+ author.EXPECT().Update(gomock.Any(), domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(nil)
+ reader := artrepomocks.NewMockArticleReaderRepository(ctrl)
+ reader.EXPECT().Save(gomock.Any(), domain.Article{
+ // 确保使用了制作库 ID
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Times(3).Return(int64(0), errors.New("mock db error"))
+ return author, reader
+ },
+ art: domain.Article{
+ Id: 2,
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ },
+ wantId: 0,
+ wantErr: errors.New("mock db error"),
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ author, reader := tc.mock(ctrl)
+ svc := NewArticleServiceV1(author, reader, &logger.NopLogger{})
+ id, err := svc.PublishV1(context.Background(), tc.art)
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantId, id)
+ })
+ }
+}
diff --git a/webook/internal/service/code.go b/webook/internal/service/code.go
new file mode 100644
index 0000000000000000000000000000000000000000..9b23da73a1e9921b0ccdf3fccdb8168b5159aa5c
--- /dev/null
+++ b/webook/internal/service/code.go
@@ -0,0 +1,93 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "go.uber.org/atomic"
+ "math/rand"
+)
+
+var codeTplId atomic.String = atomic.String{}
+
+var (
+ ErrCodeVerifyTooManyTimes = repository.ErrCodeVerifyTooManyTimes
+ ErrCodeSendTooMany = repository.ErrCodeSendTooMany
+)
+
+type CodeService interface {
+ Send(ctx context.Context,
+ // 区别业务场景
+ biz string, phone string) error
+ Verify(ctx context.Context, biz string,
+ phone string, inputCode string) (bool, error)
+}
+
+type codeService struct {
+ repo repository.CodeRepository
+ smsSvc sms.Service
+ //tplId string
+}
+
+func NewCodeService(repo repository.CodeRepository, smsSvc sms.Service) CodeService {
+ codeTplId.Store("1877556")
+ //viper.OnConfigChange(func(in fsnotify.Event) {
+ // codeTplId.Store(viper.GetString("code.tpl.id"))
+ //})
+
+ return &codeService{
+ repo: repo,
+ smsSvc: smsSvc,
+ }
+}
+
+// Send 发验证码,我需要什么参数?
+func (svc *codeService) Send(ctx context.Context,
+ // 区别业务场景
+ biz string,
+ phone string) error {
+ // 生成一个验证码
+ code := svc.generateCode()
+ // 塞进去 Redis
+ err := svc.repo.Store(ctx, biz, phone, code)
+ if err != nil {
+ // 有问题
+ return err
+ }
+ // 这前面成功了
+
+ // 发送出去
+
+ err = svc.smsSvc.Send(ctx, codeTplId.Load(), []string{code}, phone)
+ if err != nil {
+ err = fmt.Errorf("发送短信出现异常 %w", err)
+ }
+ //if err != nil {
+ // 这个地方怎么办?
+ // 这意味着,Redis 有这个验证码,但是不好意思,
+ // 我能不能删掉这个验证码?
+ // 你这个 err 可能是超时的 err,你都不知道,发出了没
+ // 在这里重试
+ // 要重试的话,初始化的时候,传入一个自己就会重试的 smsSvc
+ //}
+ return err
+}
+
+func (svc *codeService) Verify(ctx context.Context, biz string,
+ phone string, inputCode string) (bool, error) {
+ return svc.repo.Verify(ctx, biz, phone, inputCode)
+}
+
+func (svc *codeService) generateCode() string {
+ // 六位数,num 在 0, 999999 之间,包含 0 和 999999
+ num := rand.Intn(1000000)
+ // 不够六位的,加上前导 0
+ // 000001
+ return fmt.Sprintf("%06d", num)
+}
+
+//func (svc *codeService) VerifyV1(ctx context.Context, biz string,
+// phone string, inputCode string) error {
+//
+//}
diff --git a/webook/internal/service/code_test.go b/webook/internal/service/code_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a70bcecc1a26e1db2484a45f28fe2fb73ca3258
--- /dev/null
+++ b/webook/internal/service/code_test.go
@@ -0,0 +1,10 @@
+package service
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestFormate(t *testing.T) {
+ t.Log(fmt.Sprintf("%06d", 10))
+}
diff --git a/webook/internal/service/job.go b/webook/internal/service/job.go
new file mode 100644
index 0000000000000000000000000000000000000000..b4799418408089730a1bf179b31e6e366ceb652b
--- /dev/null
+++ b/webook/internal/service/job.go
@@ -0,0 +1,88 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "time"
+)
+
+type JobService interface {
+ // Preempt 抢占
+ Preempt(ctx context.Context) (domain.Job, error)
+ ResetNextTime(ctx context.Context, j domain.Job) error
+ // 我返回一个释放的方法,然后调用者取调
+ // PreemptV1(ctx context.Context) (domain.Job, func() error, error)
+ // Release
+ //Release(ctx context.Context, id int64) error
+}
+
+type cronJobService struct {
+ repo repository.JobRepository
+ refreshInterval time.Duration
+ l logger.LoggerV1
+}
+
+func (p *cronJobService) Preempt(ctx context.Context) (domain.Job, error) {
+ j, err := p.repo.Preempt(ctx)
+
+ // 你的续约呢?
+ //ch := make(chan struct{})
+ //go func() {
+ // ticker := time.NewTicker(p.refreshInterval)
+ // for {
+ // select {
+ // case <-ticker.C:
+ // // 在这里续约
+ // p.refresh(j.Id)
+ // case <-ch:
+ // // 结束
+ // return
+ // }
+ // }
+ //}()
+
+ ticker := time.NewTicker(p.refreshInterval)
+ go func() {
+ for range ticker.C {
+ p.refresh(j.Id)
+ }
+ }()
+
+ // 你抢占之后,你一直抢占着吗?
+ // 你要考虑一个释放的问题
+ j.CancelFunc = func() error {
+ //close(ch)
+ // 自己在这里释放掉
+ ticker.Stop()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return p.repo.Release(ctx, j.Id)
+ }
+ return j, err
+}
+
+func (p *cronJobService) ResetNextTime(ctx context.Context, j domain.Job) error {
+ next := j.NextTime()
+ if next.IsZero() {
+ // 没有下一次
+ return p.repo.Stop(ctx, j.Id)
+ }
+ return p.repo.UpdateNextTime(ctx, j.Id, next)
+}
+
+func (p *cronJobService) refresh(id int64) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ // 续约怎么个续法?
+ // 更新一下更新时间就可以
+ // 比如说我们的续约失败逻辑就是:处于 running 状态,但是更新时间在三分钟以前
+ err := p.repo.UpdateUtime(ctx, id)
+ if err != nil {
+ // 可以考虑立刻重试
+ p.l.Error("续约失败",
+ logger.Error(err),
+ logger.Int64("jid", id))
+ }
+}
diff --git a/webook/internal/service/mocks/article.mock.go b/webook/internal/service/mocks/article.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..ce3df89be375b16574dc015865a2af006dda22af
--- /dev/null
+++ b/webook/internal/service/mocks/article.mock.go
@@ -0,0 +1,160 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: article.go
+//
+// Generated by this command:
+//
+// mockgen -source=article.go -package=svcmocks -destination=mocks/article.mock.go ArticleService
+//
+// Package svcmocks is a generated GoMock package.
+package svcmocks
+
+import (
+ context "context"
+ reflect "reflect"
+ time "time"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockArticleService is a mock of ArticleService interface.
+type MockArticleService struct {
+ ctrl *gomock.Controller
+ recorder *MockArticleServiceMockRecorder
+}
+
+// MockArticleServiceMockRecorder is the mock recorder for MockArticleService.
+type MockArticleServiceMockRecorder struct {
+ mock *MockArticleService
+}
+
+// NewMockArticleService creates a new mock instance.
+func NewMockArticleService(ctrl *gomock.Controller) *MockArticleService {
+ mock := &MockArticleService{ctrl: ctrl}
+ mock.recorder = &MockArticleServiceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockArticleService) EXPECT() *MockArticleServiceMockRecorder {
+ return m.recorder
+}
+
+// GetById mocks base method.
+func (m *MockArticleService) GetById(ctx context.Context, id int64) (domain.Article, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetById", ctx, id)
+ ret0, _ := ret[0].(domain.Article)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetById indicates an expected call of GetById.
+func (mr *MockArticleServiceMockRecorder) GetById(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetById", reflect.TypeOf((*MockArticleService)(nil).GetById), ctx, id)
+}
+
+// GetPublishedById mocks base method.
+func (m *MockArticleService) GetPublishedById(ctx context.Context, id, uid int64) (domain.Article, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPublishedById", ctx, id, uid)
+ ret0, _ := ret[0].(domain.Article)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPublishedById indicates an expected call of GetPublishedById.
+func (mr *MockArticleServiceMockRecorder) GetPublishedById(ctx, id, uid any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishedById", reflect.TypeOf((*MockArticleService)(nil).GetPublishedById), ctx, id, uid)
+}
+
+// List mocks base method.
+func (m *MockArticleService) List(ctx context.Context, uid int64, offset, limit int) ([]domain.Article, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "List", ctx, uid, offset, limit)
+ ret0, _ := ret[0].([]domain.Article)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockArticleServiceMockRecorder) List(ctx, uid, offset, limit any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockArticleService)(nil).List), ctx, uid, offset, limit)
+}
+
+// ListPub mocks base method.
+func (m *MockArticleService) ListPub(ctx context.Context, start time.Time, offset, limit int) ([]domain.Article, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPub", ctx, start, offset, limit)
+ ret0, _ := ret[0].([]domain.Article)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPub indicates an expected call of ListPub.
+func (mr *MockArticleServiceMockRecorder) ListPub(ctx, start, offset, limit any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPub", reflect.TypeOf((*MockArticleService)(nil).ListPub), ctx, start, offset, limit)
+}
+
+// Publish mocks base method.
+func (m *MockArticleService) Publish(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Publish", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Publish indicates an expected call of Publish.
+func (mr *MockArticleServiceMockRecorder) Publish(ctx, art any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockArticleService)(nil).Publish), ctx, art)
+}
+
+// PublishV1 mocks base method.
+func (m *MockArticleService) PublishV1(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PublishV1", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PublishV1 indicates an expected call of PublishV1.
+func (mr *MockArticleServiceMockRecorder) PublishV1(ctx, art any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishV1", reflect.TypeOf((*MockArticleService)(nil).PublishV1), ctx, art)
+}
+
+// Save mocks base method.
+func (m *MockArticleService) Save(ctx context.Context, art domain.Article) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", ctx, art)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Save indicates an expected call of Save.
+func (mr *MockArticleServiceMockRecorder) Save(ctx, art any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockArticleService)(nil).Save), ctx, art)
+}
+
+// Withdraw mocks base method.
+func (m *MockArticleService) Withdraw(ctx context.Context, art domain.Article) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Withdraw", ctx, art)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Withdraw indicates an expected call of Withdraw.
+func (mr *MockArticleServiceMockRecorder) Withdraw(ctx, art any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockArticleService)(nil).Withdraw), ctx, art)
+}
diff --git a/webook/internal/service/mocks/code.mock.go b/webook/internal/service/mocks/code.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..bbd97cd96d0f1ac2050984e9959d89f9d98cec27
--- /dev/null
+++ b/webook/internal/service/mocks/code.mock.go
@@ -0,0 +1,64 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/service/code.go
+
+// Package svcmocks is a generated GoMock package.
+package svcmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockCodeService is a mock of CodeService interface.
+type MockCodeService struct {
+ ctrl *gomock.Controller
+ recorder *MockCodeServiceMockRecorder
+}
+
+// MockCodeServiceMockRecorder is the mock recorder for MockCodeService.
+type MockCodeServiceMockRecorder struct {
+ mock *MockCodeService
+}
+
+// NewMockCodeService creates a new mock instance.
+func NewMockCodeService(ctrl *gomock.Controller) *MockCodeService {
+ mock := &MockCodeService{ctrl: ctrl}
+ mock.recorder = &MockCodeServiceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCodeService) EXPECT() *MockCodeServiceMockRecorder {
+ return m.recorder
+}
+
+// Send mocks base method.
+func (m *MockCodeService) Send(ctx context.Context, biz, phone string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Send", ctx, biz, phone)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Send indicates an expected call of Send.
+func (mr *MockCodeServiceMockRecorder) Send(ctx, biz, phone interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockCodeService)(nil).Send), ctx, biz, phone)
+}
+
+// Verify mocks base method.
+func (m *MockCodeService) Verify(ctx context.Context, biz, phone, inputCode string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Verify", ctx, biz, phone, inputCode)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Verify indicates an expected call of Verify.
+func (mr *MockCodeServiceMockRecorder) Verify(ctx, biz, phone, inputCode interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockCodeService)(nil).Verify), ctx, biz, phone, inputCode)
+}
diff --git a/webook/internal/service/mocks/interactive.mock.go b/webook/internal/service/mocks/interactive.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..ea83c9dcae4d10616ce438c249e169c58afaf996
--- /dev/null
+++ b/webook/internal/service/mocks/interactive.mock.go
@@ -0,0 +1,126 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./interactive.go
+//
+// Generated by this command:
+//
+// mockgen -source=./interactive.go -package=svcmocks -destination=mocks/interactive.mock.go InteractiveService
+//
+// Package svcmocks is a generated GoMock package.
+package svcmocks
+
+import (
+ context "context"
+ "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockInteractiveService is a mock of InteractiveService interface.
+type MockInteractiveService struct {
+ ctrl *gomock.Controller
+ recorder *MockInteractiveServiceMockRecorder
+}
+
+// MockInteractiveServiceMockRecorder is the mock recorder for MockInteractiveService.
+type MockInteractiveServiceMockRecorder struct {
+ mock *MockInteractiveService
+}
+
+// NewMockInteractiveService creates a new mock instance.
+func NewMockInteractiveService(ctrl *gomock.Controller) *MockInteractiveService {
+ mock := &MockInteractiveService{ctrl: ctrl}
+ mock.recorder = &MockInteractiveServiceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInteractiveService) EXPECT() *MockInteractiveServiceMockRecorder {
+ return m.recorder
+}
+
+// CancelLike mocks base method.
+func (m *MockInteractiveService) CancelLike(ctx context.Context, biz string, bizId, uid int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CancelLike", ctx, biz, bizId, uid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CancelLike indicates an expected call of CancelLike.
+func (mr *MockInteractiveServiceMockRecorder) CancelLike(ctx, biz, bizId, uid any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelLike", reflect.TypeOf((*MockInteractiveService)(nil).CancelLike), ctx, biz, bizId, uid)
+}
+
+// Collect mocks base method.
+func (m *MockInteractiveService) Collect(ctx context.Context, biz string, bizId, cid, uid int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Collect", ctx, biz, bizId, cid, uid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Collect indicates an expected call of Collect.
+func (mr *MockInteractiveServiceMockRecorder) Collect(ctx, biz, bizId, cid, uid any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collect", reflect.TypeOf((*MockInteractiveService)(nil).Collect), ctx, biz, bizId, cid, uid)
+}
+
+// Get mocks base method.
+func (m *MockInteractiveService) Get(ctx context.Context, biz string, bizId, uid int64) (domain.Interactive, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", ctx, biz, bizId, uid)
+ ret0, _ := ret[0].(domain.Interactive)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockInteractiveServiceMockRecorder) Get(ctx, biz, bizId, uid any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInteractiveService)(nil).Get), ctx, biz, bizId, uid)
+}
+
+// GetByIds mocks base method.
+func (m *MockInteractiveService) GetByIds(ctx context.Context, biz string, bizIds []int64) (map[int64]domain.Interactive, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetByIds", ctx, biz, bizIds)
+ ret0, _ := ret[0].(map[int64]domain.Interactive)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetByIds indicates an expected call of GetByIds.
+func (mr *MockInteractiveServiceMockRecorder) GetByIds(ctx, biz, bizIds any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIds", reflect.TypeOf((*MockInteractiveService)(nil).GetByIds), ctx, biz, bizIds)
+}
+
+// IncrReadCnt mocks base method.
+func (m *MockInteractiveService) IncrReadCnt(ctx context.Context, biz string, bizId int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IncrReadCnt", ctx, biz, bizId)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// IncrReadCnt indicates an expected call of IncrReadCnt.
+func (mr *MockInteractiveServiceMockRecorder) IncrReadCnt(ctx, biz, bizId any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrReadCnt", reflect.TypeOf((*MockInteractiveService)(nil).IncrReadCnt), ctx, biz, bizId)
+}
+
+// Like mocks base method.
+func (m *MockInteractiveService) Like(ctx context.Context, biz string, bizId, uid int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Like", ctx, biz, bizId, uid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Like indicates an expected call of Like.
+func (mr *MockInteractiveServiceMockRecorder) Like(ctx, biz, bizId, uid any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Like", reflect.TypeOf((*MockInteractiveService)(nil).Like), ctx, biz, bizId, uid)
+}
diff --git a/webook/internal/service/mocks/user.mock.go b/webook/internal/service/mocks/user.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..231dbade06b4dfa68cd4bd1d936498d3d21fc0ae
--- /dev/null
+++ b/webook/internal/service/mocks/user.mock.go
@@ -0,0 +1,110 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/internal/service/user.go
+
+// Package svcmocks is a generated GoMock package.
+package svcmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/internal/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockUserService is a mock of UserService interface.
+type MockUserService struct {
+ ctrl *gomock.Controller
+ recorder *MockUserServiceMockRecorder
+}
+
+// MockUserServiceMockRecorder is the mock recorder for MockUserService.
+type MockUserServiceMockRecorder struct {
+ mock *MockUserService
+}
+
+// NewMockUserService creates a new mock instance.
+func NewMockUserService(ctrl *gomock.Controller) *MockUserService {
+ mock := &MockUserService{ctrl: ctrl}
+ mock.recorder = &MockUserServiceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockUserService) EXPECT() *MockUserServiceMockRecorder {
+ return m.recorder
+}
+
+// FindOrCreate mocks base method.
+func (m *MockUserService) FindOrCreate(ctx context.Context, phone string) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindOrCreate", ctx, phone)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindOrCreate indicates an expected call of FindOrCreate.
+func (mr *MockUserServiceMockRecorder) FindOrCreate(ctx, phone interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOrCreate", reflect.TypeOf((*MockUserService)(nil).FindOrCreate), ctx, phone)
+}
+
+// FindOrCreateByWechat mocks base method.
+func (m *MockUserService) FindOrCreateByWechat(ctx context.Context, wechatInfo domain.WechatInfo) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindOrCreateByWechat", ctx, wechatInfo)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindOrCreateByWechat indicates an expected call of FindOrCreateByWechat.
+func (mr *MockUserServiceMockRecorder) FindOrCreateByWechat(ctx, wechatInfo interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOrCreateByWechat", reflect.TypeOf((*MockUserService)(nil).FindOrCreateByWechat), ctx, wechatInfo)
+}
+
+// Login mocks base method.
+func (m *MockUserService) Login(ctx context.Context, email, password string) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Login", ctx, email, password)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Login indicates an expected call of Login.
+func (mr *MockUserServiceMockRecorder) Login(ctx, email, password interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUserService)(nil).Login), ctx, email, password)
+}
+
+// Profile mocks base method.
+func (m *MockUserService) Profile(ctx context.Context, id int64) (domain.User, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Profile", ctx, id)
+ ret0, _ := ret[0].(domain.User)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Profile indicates an expected call of Profile.
+func (mr *MockUserServiceMockRecorder) Profile(ctx, id interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockUserService)(nil).Profile), ctx, id)
+}
+
+// SignUp mocks base method.
+func (m *MockUserService) SignUp(ctx context.Context, u domain.User) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SignUp", ctx, u)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SignUp indicates an expected call of SignUp.
+func (mr *MockUserServiceMockRecorder) SignUp(ctx, u interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignUp", reflect.TypeOf((*MockUserService)(nil).SignUp), ctx, u)
+}
diff --git a/webook/internal/service/oauth2/wechat/service.go b/webook/internal/service/oauth2/wechat/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6062ef3af9a8fbf830f818c91a64a2473ea4947
--- /dev/null
+++ b/webook/internal/service/oauth2/wechat/service.go
@@ -0,0 +1,116 @@
+package wechat
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "go.uber.org/zap"
+ "net/http"
+ "net/url"
+)
+
+var redirectURI = url.PathEscape("https://meoying.com/oauth2/wechat/callback")
+
+type Service interface {
+ AuthURL(ctx context.Context, state string) (string, error)
+ VerifyCode(ctx context.Context, code string) (domain.WechatInfo, error)
+}
+
+type service struct {
+ appId string
+ appSecret string
+ client *http.Client
+ //cmd redis.Cmdable
+ l logger.LoggerV1
+}
+
+// 不偷懒的写法
+func NewServiceV1(appId string, appSecret string, client *http.Client) Service {
+ return &service{
+ appId: appId,
+ appSecret: appSecret,
+ client: client,
+ }
+}
+
+func NewService(appId string, appSecret string, l logger.LoggerV1) Service {
+ return &service{
+ appId: appId,
+ appSecret: appSecret,
+ // 依赖注入,但是没完全注入
+ client: http.DefaultClient,
+ l: l,
+ }
+}
+
+func (s *service) VerifyCode(ctx context.Context, code string) (domain.WechatInfo, error) {
+ const targetPattern = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
+ target := fmt.Sprintf(targetPattern, s.appId, s.appSecret, code)
+ //resp, err := http.Get(target)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
+ //req, err := http.NewRequest(http.MethodGet, target, nil)
+ if err != nil {
+ return domain.WechatInfo{}, err
+ }
+ // 会产生复制,性能极差,比如说你的 URL 很长
+ //req = req.WithContext(ctx)
+
+ resp, err := s.client.Do(req)
+ if err != nil {
+ return domain.WechatInfo{}, err
+ }
+
+ // 只读一遍
+ decoder := json.NewDecoder(resp.Body)
+ var res Result
+ err = decoder.Decode(&res)
+
+ // 整个响应都读出来,不推荐,因为 Unmarshal 再读一遍,合计两遍
+ //body, err := io.ReadAll(resp.Body)
+ //err = json.Unmarshal(body, &res)
+
+ if err != nil {
+ return domain.WechatInfo{}, err
+ }
+
+ if res.ErrCode != 0 {
+ return domain.WechatInfo{},
+ fmt.Errorf("微信返回错误响应,错误码:%d,错误信息:%s", res.ErrCode, res.ErrMsg)
+ }
+
+ // 攻击者的 state
+ //str := s.cmd.Get(ctx, "my-state"+state).String()
+ //if str != state {
+ // // 不相等
+ //}
+
+ zap.L().Info("调用微信,拿到用户信息",
+ zap.String("unionID", res.UnionID), zap.String("openID", res.OpenID))
+
+ return domain.WechatInfo{
+ OpenID: res.OpenID,
+ UnionID: res.UnionID,
+ }, nil
+}
+
+func (s *service) AuthURL(ctx context.Context, state string) (string, error) {
+ const urlPattern = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect"
+ // 如果在这里存 state,假如说我存 redis
+ //s.cmd.Set(ctx, "my-state"+state, state, time.Minute)
+ return fmt.Sprintf(urlPattern, s.appId, redirectURI, state), nil
+}
+
+type Result struct {
+ ErrCode int64 `json:"errcode"`
+ ErrMsg string `json:"errmsg"`
+
+ AccessToken string `json:"access_token"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+
+ OpenID string `json:"openid"`
+ Scope string `json:"scope"`
+ UnionID string `json:"unionid"`
+}
diff --git a/webook/internal/service/oauth2/wechat/service_test.go b/webook/internal/service/oauth2/wechat/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8ce524ab506db42dd23423bb76ce15c25a8f1bd8
--- /dev/null
+++ b/webook/internal/service/oauth2/wechat/service_test.go
@@ -0,0 +1,26 @@
+//go:build manual
+
+package wechat
+
+import (
+ "context"
+ "github.com/stretchr/testify/require"
+ "os"
+ "testing"
+)
+
+// 手动跑的。提前验证代码
+func Test_service_manual_VerifyCode(t *testing.T) {
+ appId, ok := os.LookupEnv("WECHAT_APP_ID")
+ if !ok {
+ panic("没有找到环境变量 WECHAT_APP_ID ")
+ }
+ appKey, ok := os.LookupEnv("WECHAT_APP_SECRET")
+ if !ok {
+ panic("没有找到环境变量 WECHAT_APP_SECRET")
+ }
+ svc := NewService(appId, appKey)
+ res, err := svc.VerifyCode(context.Background(), "051D6b000Yn4FQ14Rd300FgOF33D6b0s", "state")
+ require.NoError(t, err)
+ t.Log(res)
+}
diff --git a/webook/internal/service/ranking.go b/webook/internal/service/ranking.go
new file mode 100644
index 0000000000000000000000000000000000000000..61ac45ae7a9de661587aa599b5e6acea43098691
--- /dev/null
+++ b/webook/internal/service/ranking.go
@@ -0,0 +1,152 @@
+package service
+
+import (
+ "context"
+ "errors"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ "github.com/ecodeclub/ekit/queue"
+ "github.com/ecodeclub/ekit/slice"
+ "math"
+ "time"
+)
+
+type RankingService interface {
+ TopN(ctx context.Context) error
+ //TopN(ctx context.Context, n int64) error
+ //TopN(ctx context.Context, n int64) ([]domain.Article, error)
+}
+
+type BatchRankingService struct {
+ artSvc ArticleService
+ intrSvc intrv1.InteractiveServiceClient
+ repo repository.RankingRepository
+ batchSize int
+ n int
+ // scoreFunc 不能返回负数
+ scoreFunc func(t time.Time, likeCnt int64) float64
+
+ // 负载
+ load int64
+}
+
+func NewBatchRankingService(artSvc ArticleService,
+ repo repository.RankingRepository,
+ intrSvc intrv1.InteractiveServiceClient) RankingService {
+ return &BatchRankingService{
+ artSvc: artSvc,
+ intrSvc: intrSvc,
+ batchSize: 100,
+ n: 100,
+ repo: repo,
+ scoreFunc: func(t time.Time, likeCnt int64) float64 {
+ sec := time.Since(t).Seconds()
+ return float64(likeCnt-1) / math.Pow(float64(sec+2), 1.5)
+ },
+ }
+}
+
+// 准备分批
+func (svc *BatchRankingService) TopN(ctx context.Context) error {
+ arts, err := svc.topN(ctx)
+ if err != nil {
+ return err
+ }
+ // 在这里,存起来
+ return svc.repo.ReplaceTopN(ctx, arts)
+}
+
+// topN 已经搞完了
+func (svc *BatchRankingService) topN(ctx context.Context) ([]domain.Article, error) {
+ // 我只取七天内的数据
+ now := time.Now()
+ // 先拿一批数据
+ offset := 0
+ type Score struct {
+ art domain.Article
+ score float64
+ }
+ // 这里可以用非并发安全
+ topN := queue.NewConcurrentPriorityQueue[Score](svc.n,
+ func(src Score, dst Score) int {
+ if src.score > dst.score {
+ return 1
+ } else if src.score == dst.score {
+ return 0
+ } else {
+ return -1
+ }
+ })
+
+ for {
+ // 这里拿了一批
+ arts, err := svc.artSvc.ListPub(ctx, now, offset, svc.batchSize)
+ if err != nil {
+ return nil, err
+ }
+ ids := slice.Map[domain.Article, int64](arts,
+ func(idx int, src domain.Article) int64 {
+ return src.Id
+ })
+ // 要去找到对应的点赞数据
+ intrs, err := svc.intrSvc.GetByIds(ctx, &intrv1.GetByIdsRequest{
+ Biz: "article", Ids: ids,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(intrs.Intrs) == 0 {
+ return nil, errors.New("没有数据")
+ }
+ // 合并计算 score
+ // 排序
+ for _, art := range arts {
+ intr := intrs.Intrs[art.Id]
+ //if !ok {
+ // // 你都没有,肯定不可能是热榜
+ // continue
+ //}
+ score := svc.scoreFunc(art.Utime, intr.LikeCnt)
+ // 我要考虑,我这个 score 在不在前一百名
+ // 拿到热度最低的
+ err = topN.Enqueue(Score{
+ art: art,
+ score: score,
+ })
+ // 这种写法,要求 topN 已经满了
+ if err == queue.ErrOutOfCapacity {
+ val, _ := topN.Dequeue()
+ if val.score < score {
+ _ = topN.Enqueue(Score{
+ art: art,
+ score: score,
+ })
+ } else {
+ _ = topN.Enqueue(val)
+ }
+ }
+ }
+
+ // 一批已经处理完了,问题来了,我要不要进入下一批?我怎么知道还有没有?
+ if len(arts) < svc.batchSize ||
+ now.Sub(arts[len(arts)-1].Utime).Hours() > 7*24 {
+ // 我这一批都没取够,我当然可以肯定没有下一批了
+ // 又或者已经取到了七天之前的数据了,说明可以中断了
+ break
+ }
+ // 这边要更新 offset
+ offset = offset + len(arts)
+ }
+ // 最后得出结果
+ res := make([]domain.Article, svc.n)
+ for i := svc.n - 1; i >= 0; i-- {
+ val, err := topN.Dequeue()
+ if err != nil {
+ // 说明取完了,不够 n
+ break
+ }
+ res[i] = val.art
+ }
+ return res, nil
+}
diff --git a/webook/internal/service/ranking_test.go b/webook/internal/service/ranking_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..48da49c59698f01a6da1c5e91d6ee158f8eb04d1
--- /dev/null
+++ b/webook/internal/service/ranking_test.go
@@ -0,0 +1,78 @@
+//go:build need_fix
+
+package service
+
+import (
+ "context"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ domain2 "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ svcmocks "gitee.com/geekbang/basic-go/webook/internal/service/mocks"
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "testing"
+ "time"
+)
+
+func TestRankingTopN(t *testing.T) {
+ now := time.Now()
+ testCases := []struct {
+ name string
+ mock func(ctrl *gomock.Controller) (ArticleService,
+ intrv1.InteractiveServiceClient)
+
+ wantErr error
+ wantArts []domain.Article
+ }{
+ {
+ name: "计算成功",
+ // 怎么模拟我的数据?
+ mock: func(ctrl *gomock.Controller) (ArticleService, intrv1.InteractiveServiceClient) {
+ artSvc := svcmocks.NewMockArticleService(ctrl)
+ // 最简单,一批就搞完
+ artSvc.EXPECT().ListPub(gomock.Any(), gomock.Any(), 0, 3).
+ Return([]domain.Article{
+ {Id: 1, Utime: now, Ctime: now},
+ {Id: 2, Utime: now, Ctime: now},
+ {Id: 3, Utime: now, Ctime: now},
+ }, nil)
+ artSvc.EXPECT().ListPub(gomock.Any(), gomock.Any(), 3, 3).
+ Return([]domain.Article{}, nil)
+ intrSvc := svcmocks.NewMockInteractiveService(ctrl)
+ intrSvc.EXPECT().GetByIds(gomock.Any(),
+ "article", []int64{1, 2, 3}).
+ Return(map[int64]domain2.Interactive{
+ 1: {BizId: 1, LikeCnt: 1},
+ 2: {BizId: 2, LikeCnt: 2},
+ 3: {BizId: 3, LikeCnt: 3},
+ }, nil)
+ intrSvc.EXPECT().GetByIds(gomock.Any(),
+ "article", []int64{}).
+ Return(map[int64]domain2.Interactive{}, nil)
+ return artSvc, intrSvc
+ },
+ wantArts: []domain.Article{
+ {Id: 3, Utime: now, Ctime: now},
+ {Id: 2, Utime: now, Ctime: now},
+ {Id: 1, Utime: now, Ctime: now},
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ artSvc, intrSvc := tc.mock(ctrl)
+ svc := NewBatchRankingService(artSvc, intrSvc).(*BatchRankingService)
+ // 为了测试
+ svc.batchSize = 3
+ svc.n = 3
+ svc.scoreFunc = func(t time.Time, likeCnt int64) float64 {
+ return float64(likeCnt)
+ }
+ arts, err := svc.topN(context.Background())
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantArts, arts)
+ })
+ }
+}
diff --git a/webook/internal/service/sms/aliyun/service.go b/webook/internal/service/sms/aliyun/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..9d7d8d67afa736f59bc917e3edbf0877a81f5a12
--- /dev/null
+++ b/webook/internal/service/sms/aliyun/service.go
@@ -0,0 +1,65 @@
+package aliyun
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ sms "github.com/alibabacloud-go/dysmsapi-20170525/v2/client"
+ "github.com/ecodeclub/ekit"
+ "github.com/goccy/go-json"
+ "math/rand"
+ "time"
+)
+
+/**
+ @author:biguanqun
+ @since: 2023/8/20
+ @desc:
+**/
+
+type Service struct {
+ client *sms.Client
+}
+
+func NewService(client *sms.Client) *Service {
+ return &Service{
+ client: client,
+ }
+}
+
+// SendSms 单次
+func (s *Service) SendSms(ctx context.Context, signName, tplCode string, phone []string) error {
+ phoneLen := len(phone)
+
+ // phone1 phone2
+ // 0 1
+ for i := 0; i < phoneLen; i++ {
+ phoneSignle := phone[i]
+
+ // 1. 生成验证码
+ code := fmt.Sprintf("%06v",
+ rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
+
+ // 完全没有做成一个独立的发短信的实现。而是一个强耦合验证码的实现。
+ bcode, _ := json.Marshal(map[string]interface{}{
+ "code": code,
+ })
+
+ // 2. 初始化短信结构体
+ smsRequest := &sms.SendSmsRequest{
+ SignName: ekit.ToPtr[string](signName),
+ TemplateCode: ekit.ToPtr[string](tplCode),
+ PhoneNumbers: ekit.ToPtr[string](phoneSignle),
+ TemplateParam: ekit.ToPtr[string](string(bcode)),
+ }
+
+ // 3. 发送短信
+ smsResponse, _ := s.client.SendSms(smsRequest)
+ if *smsResponse.Body.Code == "OK" {
+ fmt.Println(phoneSignle, string(bcode))
+ fmt.Printf("发送手机号: %s 的短信成功,验证码为【%s】\n", phoneSignle, code)
+ }
+ fmt.Println(errors.New(*smsResponse.Body.Message))
+ }
+ return nil
+}
diff --git a/webook/internal/service/sms/aliyun/service_test.go b/webook/internal/service/sms/aliyun/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a558108de95e71d60611d874d01e5a742f824246
--- /dev/null
+++ b/webook/internal/service/sms/aliyun/service_test.go
@@ -0,0 +1,86 @@
+package aliyun
+
+import (
+ "context"
+ openapi "github.com/alibabacloud-go/darabonba-openapi/client"
+ sms "github.com/alibabacloud-go/dysmsapi-20170525/v2/client"
+ "github.com/ecodeclub/ekit"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+/**
+ @author:biguanqun
+ @since: 2023/8/20
+ @desc:
+**/
+
+//func TestSender(t *testing.T) {
+//
+// keyId := ""
+// keySecret := ""
+//
+// config := &openapi.Config{
+// AccessKeyId: ekit.ToPtr[string](keyId),
+// AccessKeySecret: ekit.ToPtr[string](keySecret),
+// }
+// client, err := sms.NewClient(config)
+// if err != nil {
+// t.Fatal(err)
+// }
+// service := NewService(client)
+//
+// testCases := []struct {
+// signName string
+// tplCode string
+// phone string
+// wantErr error
+// }{
+// {
+// signName: "webook",
+// tplCode: "SMS_462745194",
+// phone: "",
+// },
+// }
+// for _, tc := range testCases {
+// t.Run(tc.signName, func(t *testing.T) {
+// er := service.SendSms(context.Background(), tc.signName, tc.tplCode, tc.phone)
+// assert.Equal(t, tc.wantErr, er)
+// })
+// }
+//}
+
+func TestService_SendSms(t *testing.T) {
+
+ keyId := ""
+ keySecret := ""
+
+ config := &openapi.Config{
+ AccessKeyId: ekit.ToPtr[string](keyId),
+ AccessKeySecret: ekit.ToPtr[string](keySecret),
+ }
+ client, err := sms.NewClient(config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ service := NewService(client)
+
+ tests := []struct {
+ signName string
+ tplCode string
+ phone []string
+ wantErr error
+ }{
+ {
+ signName: "",
+ tplCode: "",
+ phone: []string{"", ""},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.signName, func(t *testing.T) {
+ er := service.SendSms(context.Background(), tt.signName, tt.tplCode, tt.phone)
+ assert.Equal(t, tt.wantErr, er)
+ })
+ }
+}
diff --git a/webook/internal/service/sms/aliyunv1/service.go b/webook/internal/service/sms/aliyunv1/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf6463dc85c6d2af74a29f18f50a57e4d171232b
--- /dev/null
+++ b/webook/internal/service/sms/aliyunv1/service.go
@@ -0,0 +1,119 @@
+package aliyunv1
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
+ "strconv"
+ "strings"
+)
+
+type Service struct {
+ client *dysmsapi.Client
+ signName string
+}
+
+func NewService(c *dysmsapi.Client, signName string) *Service {
+ return &Service{
+ client: c,
+ signName: signName,
+ }
+}
+
+// []string
+func (s *Service) SendOrigin(ctx context.Context, tplId string,
+ args map[string]string, numbers ...string) error {
+ req := dysmsapi.CreateSendSmsRequest()
+ req.Scheme = "https"
+ // 阿里云多个手机号为字符串逗号间隔
+ req.PhoneNumbers = strings.Join(numbers, ",")
+ req.SignName = s.signName
+ // 这意味着,你的模板必须是 你的短信验证码是{0}
+ // 你的短信验证码是{code}
+ bCode, err := json.Marshal(args)
+ if err != nil {
+ return err
+ }
+ req.TemplateParam = string(bCode)
+ req.TemplateCode = tplId
+
+ var resp *dysmsapi.SendSmsResponse
+ resp, err = s.client.SendSms(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.Code != "OK" {
+ return fmt.Errorf("发送失败,code: %s, 原因:%s",
+ resp.Code, resp.Message)
+ }
+ return nil
+}
+
+func (s *Service) Send(ctx context.Context, tplId string, args []string, numbers ...string) error {
+ req := dysmsapi.CreateSendSmsRequest()
+ req.Scheme = "https"
+ // 阿里云多个手机号为字符串逗号间隔
+ req.PhoneNumbers = strings.Join(numbers, ",")
+ req.SignName = s.signName
+ // 传的是 JSON
+ argsMap := make(map[string]string, len(args))
+ for idx, arg := range args {
+ argsMap[strconv.Itoa(idx)] = arg
+ }
+ // 这意味着,你的模板必须是 你的短信验证码是{0}
+ // 你的短信验证码是{code}
+ bCode, err := json.Marshal(argsMap)
+ if err != nil {
+ return err
+ }
+ req.TemplateParam = string(bCode)
+ req.TemplateCode = tplId
+
+ var resp *dysmsapi.SendSmsResponse
+ resp, err = s.client.SendSms(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.Code != "OK" {
+ return fmt.Errorf("发送失败,code: %s, 原因:%s",
+ resp.Code, resp.Message)
+ }
+ return nil
+}
+
+func (s *Service) SendV1(ctx context.Context, tplId string, args []sms.NamedArg, numbers ...string) error {
+ req := dysmsapi.CreateSendSmsRequest()
+ req.Scheme = "https"
+ // 阿里云多个手机号为字符串逗号间隔
+ req.PhoneNumbers = strings.Join(numbers, ",")
+ req.SignName = s.signName
+ // 传的是 JSON
+ argsMap := make(map[string]string, len(args))
+ for _, arg := range args {
+ argsMap[arg.Name] = arg.Val
+ }
+ // 这意味着,你的模板必须是 你的短信验证码是{0}
+ // 你的短信验证码是{code}
+ bCode, err := json.Marshal(argsMap)
+ if err != nil {
+ return err
+ }
+ req.TemplateParam = string(bCode)
+ req.TemplateCode = tplId
+
+ var resp *dysmsapi.SendSmsResponse
+ resp, err = s.client.SendSms(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.Code != "OK" {
+ return fmt.Errorf("发送失败,code: %s, 原因:%s",
+ resp.Code, resp.Message)
+ }
+ return nil
+}
diff --git a/webook/internal/service/sms/async/service.go b/webook/internal/service/sms/async/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8933e404b7a4dfd67664cc8c045585dc0fd1fc2
--- /dev/null
+++ b/webook/internal/service/sms/async/service.go
@@ -0,0 +1,32 @@
+package async
+
+//type SMSService struct {
+// svc sms.Service
+// repo repository.SMSAysncReqRepository
+//}
+//
+//func NewSMSService() *SMSService {
+// return &SMSService{}
+//}
+//func (s *SMSService) StartAsync() {
+// go func() {
+// reqs := s.repo.Find没法出去的请求()
+// for _, req := range reqs {
+// // 在这里发送,并且控制重试
+// s.svc.Send(, req.biz, req.args, req.numbers...)
+// }
+// }()
+//}
+//
+//func (s *SMSService) Send(ctx context.Context, biz string, args []string, numbers ...string) error {
+// // 首先是正常路径
+// err := s.svc.Send(ctx, biz, args, numbers...)
+// if err != nil {
+// // 判定是不是崩溃了
+//
+// if 崩溃了 {
+// s.repo.Store()
+// }
+// }
+// return
+//}
diff --git a/webook/internal/service/sms/auth/service.go b/webook/internal/service/sms/auth/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..2508d6ace8b677df7f007f67442e056f5639a844
--- /dev/null
+++ b/webook/internal/service/sms/auth/service.go
@@ -0,0 +1,43 @@
+package auth
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+type SMSService struct {
+ svc sms.Service
+ key string
+}
+
+//
+//func (s *SMSService) GenerateToken(ctx context.Context, tplId string) (string, error) {
+//
+//}
+
+// Send 发送,其中 biz 必须是线下申请的一个代表业务方的 token
+func (s *SMSService) Send(ctx context.Context, biz string,
+ args []string, numbers ...string) error {
+ var tc Claims
+ // 是不是就在这?
+ // 如果我这里能解析成功,说明就是对应的业务方
+ // 没有 error 就说明,token 是我发的
+ token, err := jwt.ParseWithClaims(biz, &tc, func(token *jwt.Token) (interface{}, error) {
+ return s.key, nil
+ })
+ if err != nil {
+ return err
+ }
+ if !token.Valid {
+ return errors.New("token 不合法")
+ }
+
+ return s.svc.Send(ctx, tc.Tpl, args, numbers...)
+}
+
+type Claims struct {
+ jwt.RegisteredClaims
+ Tpl string
+}
diff --git a/webook/internal/service/sms/cloopen/service.go b/webook/internal/service/sms/cloopen/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..52ab8a8b1182aa6182b464c65cad9c651b6348e2
--- /dev/null
+++ b/webook/internal/service/sms/cloopen/service.go
@@ -0,0 +1,51 @@
+// Package cloopen 容联云短信的实现
+// SDK文档:https://doc.yuntongxun.com/pe/5f029a06a80948a1006e7760
+package cloopen
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "github.com/cloopen/go-sms-sdk/cloopen"
+)
+
+type Service struct {
+ client *cloopen.SMS
+ appId string
+}
+
+func NewService(c *cloopen.SMS, addId string) *Service {
+ return &Service{
+ client: c,
+ appId: addId,
+ }
+}
+
+func (s *Service) Send(ctx context.Context, tplId string, data []string, numbers ...string) error {
+ input := &cloopen.SendRequest{
+ // 应用的APPID
+ AppId: s.appId,
+ // 模版ID
+ TemplateId: tplId,
+ // 模版变量内容 非必填
+ Datas: data,
+ }
+
+ for _, number := range numbers {
+ // 手机号码
+ input.To = number
+
+ resp, err := s.client.Send(input)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != "000000" {
+ log.Printf("response code: %s, msg: %s \n", resp.StatusCode, resp.StatusMsg)
+ fmt.Errorf("发送失败,code: %s, 原因:%s",
+ resp.StatusCode, resp.StatusMsg)
+ }
+ }
+ return nil
+}
diff --git a/webook/internal/service/sms/cloopen/service_test.go b/webook/internal/service/sms/cloopen/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b04a33a0cd4232d95bb391b604b3612941416f59
--- /dev/null
+++ b/webook/internal/service/sms/cloopen/service_test.go
@@ -0,0 +1,48 @@
+package cloopen
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/cloopen/go-sms-sdk/cloopen"
+)
+
+func TestSender(t *testing.T) {
+ accountSId, ok := os.LookupEnv("SMS_ACCOUNT_SID")
+ if !ok {
+ t.Fatal()
+ }
+ authToken, ok := os.LookupEnv("SMS_AUTH_TOKEN")
+ appId, ok := os.LookupEnv("APP_ID")
+ number, ok := os.LookupEnv("NUMBER")
+
+ cfg := cloopen.DefaultConfig().
+ WithAPIAccount(accountSId).
+ WithAPIToken(authToken)
+ c := cloopen.NewJsonClient(cfg).SMS()
+
+ s := NewService(c, appId)
+
+ tests := []struct {
+ name string
+ tplId string
+ data []string
+ numbers []string
+ wantErr error
+ }{
+ {
+ name: "发送验证码",
+ tplId: "1",
+ data: []string{"1234", "5"},
+ // 改成你的手机号码
+ numbers: []string{number},
+ },
+ }
+ for _, tt := range tests {
+ err := s.Send(context.Background(), tt.tplId, tt.data, tt.numbers...)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/webook/internal/service/sms/failover/service.go b/webook/internal/service/sms/failover/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..6e004c20909fc7f089685953d5bb7db8ea9637dc
--- /dev/null
+++ b/webook/internal/service/sms/failover/service.go
@@ -0,0 +1,54 @@
+package failover
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "log"
+ "sync/atomic"
+)
+
+type FailoverSMSService struct {
+ svcs []sms.Service
+
+ idx uint64
+}
+
+func NewFailoverSMSService(svcs []sms.Service) sms.Service {
+ return &FailoverSMSService{
+ svcs: svcs,
+ }
+}
+
+func (f *FailoverSMSService) Send(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ for _, svc := range f.svcs {
+ err := svc.Send(ctx, tpl, args, numbers...)
+ // 发送成功
+ if err == nil {
+ return nil
+ }
+ // 正常这边,输出日志
+ // 要做好监控
+ log.Println(err)
+ }
+ return errors.New("全部服务商都失败了")
+}
+
+func (f *FailoverSMSService) SendV1(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ // 我取下一个节点来作为起始节点
+ idx := atomic.AddUint64(&f.idx, 1)
+ length := uint64(len(f.svcs))
+ for i := idx; i < idx+length; i++ {
+ svc := f.svcs[int(i%length)]
+ err := svc.Send(ctx, tpl, args, numbers...)
+ switch err {
+ case nil:
+ return nil
+ case context.DeadlineExceeded, context.Canceled:
+ return err
+ default:
+ // 输出日志
+ }
+ }
+ return errors.New("全部服务商都失败了")
+}
diff --git a/webook/internal/service/sms/failover/timeout_failover.go b/webook/internal/service/sms/failover/timeout_failover.go
new file mode 100644
index 0000000000000000000000000000000000000000..9cd75c5639d9460ec11584f0ac1cc656052f2b76
--- /dev/null
+++ b/webook/internal/service/sms/failover/timeout_failover.go
@@ -0,0 +1,60 @@
+package failover
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "sync/atomic"
+)
+
+type TimeoutFailoverSMSService struct {
+ // 你的服务商
+ svcs []sms.Service
+ idx int32
+ // 连续超时的个数
+ cnt int32
+
+ // 阈值
+ // 连续超时超过这个数字,就要切换
+ threshold int32
+}
+
+func (t *TimeoutFailoverSMSService) Send(ctx context.Context,
+ tpl string, args []string, numbers ...string) error {
+ idx := atomic.LoadInt32(&t.idx)
+ cnt := atomic.LoadInt32(&t.cnt)
+ if cnt > t.threshold {
+ // 这里要切换,新的下标,往后挪了一个
+ newIdx := (idx + 1) % int32(len(t.svcs))
+ if atomic.CompareAndSwapInt32(&t.idx, idx, newIdx) {
+ // 我成功往后挪了一位
+ atomic.StoreInt32(&t.cnt, 0)
+ }
+ // else 就是出现并发,别人换成功了
+
+ //idx = newIdx
+ idx = atomic.LoadInt32(&t.idx)
+ }
+
+ svc := t.svcs[idx]
+ err := svc.Send(ctx, tpl, args, numbers...)
+ switch err {
+ case context.DeadlineExceeded:
+ atomic.AddInt32(&t.cnt, 1)
+ return err
+ case nil:
+ // 你的连续状态被打断了
+ atomic.StoreInt32(&t.cnt, 0)
+ return nil
+ default:
+ // 不知道什么错误
+
+ // 你可以考虑,换下一个,语义则是:
+ // - 超时错误,可能是偶发的,我尽量再试试
+ // - 非超时,我直接下一个
+ return err
+ }
+}
+
+func NewTimeoutFailoverSMSService() sms.Service {
+ return &TimeoutFailoverSMSService{}
+}
diff --git a/webook/internal/service/sms/logger/service.go b/webook/internal/service/sms/logger/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..1bcbc9d52161d33d89e9e4d0348611ee4743af7a
--- /dev/null
+++ b/webook/internal/service/sms/logger/service.go
@@ -0,0 +1,21 @@
+package logger
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "go.uber.org/zap"
+)
+
+type Service struct {
+ svc sms.Service
+}
+
+func (s *Service) Send(ctx context.Context, biz string, args []string, numbers ...string) error {
+ zap.L().Debug("发送短信", zap.String("biz", biz),
+ zap.Any("args", args))
+ err := s.svc.Send(ctx, biz, args, numbers...)
+ if err != nil {
+ zap.L().Debug("发送短信出现异常", zap.Error(err))
+ }
+ return err
+}
diff --git a/webook/internal/service/sms/memory/service.go b/webook/internal/service/sms/memory/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..882e6a0932550bfcb2b1ee9b220809bed829fce4
--- /dev/null
+++ b/webook/internal/service/sms/memory/service.go
@@ -0,0 +1,18 @@
+package memory
+
+import (
+ "context"
+ "fmt"
+)
+
+type Service struct {
+}
+
+func NewService() *Service {
+ return &Service{}
+}
+
+func (s *Service) Send(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ fmt.Println(args)
+ return nil
+}
diff --git a/webook/internal/service/sms/metrics/prometheus.go b/webook/internal/service/sms/metrics/prometheus.go
new file mode 100644
index 0000000000000000000000000000000000000000..c7f2c7a2c95f8d2d03d1018e402a1b2a7c80351d
--- /dev/null
+++ b/webook/internal/service/sms/metrics/prometheus.go
@@ -0,0 +1,37 @@
+package metrics
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "github.com/prometheus/client_golang/prometheus"
+ "time"
+)
+
+type PrometheusDecorator struct {
+ svc sms.Service
+ vector *prometheus.SummaryVec
+}
+
+func NewPrometheusDecorator(svc sms.Service) *PrometheusDecorator {
+ vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Name: "sms_resp_time",
+ Help: "统计 SMS 服务的性能数据",
+ }, []string{"biz"})
+ prometheus.MustRegister(vector)
+ return &PrometheusDecorator{
+ svc: svc,
+ vector: vector,
+ }
+}
+
+func (p *PrometheusDecorator) Send(ctx context.Context,
+ biz string, args []string, numbers ...string) error {
+ startTime := time.Now()
+ defer func() {
+ duration := time.Since(startTime).Milliseconds()
+ p.vector.WithLabelValues(biz).Observe(float64(duration))
+ }()
+ return p.svc.Send(ctx, biz, args, numbers...)
+}
diff --git a/webook/internal/service/sms/opentelemetry/otel.go b/webook/internal/service/sms/opentelemetry/otel.go
new file mode 100644
index 0000000000000000000000000000000000000000..90729caf22997fe18173bb725f92877684dc1c52
--- /dev/null
+++ b/webook/internal/service/sms/opentelemetry/otel.go
@@ -0,0 +1,42 @@
+package opentelemetry
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type Service struct {
+ svc sms.Service
+ tracer trace.Tracer
+}
+
+func NewService(svc sms.Service) *Service {
+ tp := otel.GetTracerProvider()
+ tracer := tp.Tracer("gitee.com/geekbang/basic-go/webook/internal/service/sms/opentelemetry")
+ return &Service{
+ svc: svc,
+ tracer: tracer,
+ }
+}
+
+func (s *Service) Send(ctx context.Context,
+ tpl string,
+ args []string,
+ numbers ...string) error {
+ //
+ // tracer := s.tracerProvider.Tracer()
+ ctx, span := s.tracer.Start(ctx, "sms_send_"+tpl,
+ // 因为我是一个调用短信服务商的客户端
+ trace.WithSpanKind(trace.SpanKindClient),
+ )
+ defer span.End(trace.WithStackTrace(true))
+
+ err := s.svc.Send(ctx, tpl, args, numbers...)
+ if err != nil {
+ span.RecordError(err)
+ }
+
+ return err
+}
diff --git a/webook/internal/service/sms/ratelimit/service.go b/webook/internal/service/sms/ratelimit/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..8cc2b058dbc29106b8ac07725a86803fd328ebd8
--- /dev/null
+++ b/webook/internal/service/sms/ratelimit/service.go
@@ -0,0 +1,40 @@
+package ratelimit
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "gitee.com/geekbang/basic-go/webook/pkg/ratelimit"
+)
+
+var errLimited = fmt.Errorf("触发了限流")
+
+type RatelimitSMSService struct {
+ svc sms.Service
+ limiter ratelimit.Limiter
+}
+
+func NewRatelimitSMSService(svc sms.Service, limiter ratelimit.Limiter) sms.Service {
+ return &RatelimitSMSService{
+ svc: svc,
+ limiter: limiter,
+ }
+}
+
+func (s *RatelimitSMSService) Send(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ limited, err := s.limiter.Limit(ctx, "sms:tencent")
+ if err != nil {
+ // 系统错误
+ // 可以限流:保守策略,你的下游很坑的时候,
+ // 可以不限:你的下游很强,业务可用性要求很高,尽量容错策略
+ // 包一下这个错误
+ return fmt.Errorf("短信服务判断是否限流出现问题,%w", err)
+ }
+ if limited {
+ return errLimited
+ }
+ // 你这里加一些代码,新特性
+ err = s.svc.Send(ctx, tpl, args, numbers...)
+ // 你在这里也可以加一些代码,新特性
+ return err
+}
diff --git a/webook/internal/service/sms/ratelimit/service_v1.go b/webook/internal/service/sms/ratelimit/service_v1.go
new file mode 100644
index 0000000000000000000000000000000000000000..a04a73d376b98f41408fbd100c19e2f360832de4
--- /dev/null
+++ b/webook/internal/service/sms/ratelimit/service_v1.go
@@ -0,0 +1,38 @@
+package ratelimit
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "gitee.com/geekbang/basic-go/webook/pkg/ratelimit"
+)
+
+type RatelimitSMSServiceV1 struct {
+ sms.Service
+ limiter ratelimit.Limiter
+}
+
+func NewRatelimitSMSServiceV1(svc sms.Service, limiter ratelimit.Limiter) sms.Service {
+ return &RatelimitSMSService{
+ svc: svc,
+ limiter: limiter,
+ }
+}
+
+func (s *RatelimitSMSServiceV1) Send(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ limited, err := s.limiter.Limit(ctx, "sms:tencent")
+ if err != nil {
+ // 系统错误
+ // 可以限流:保守策略,你的下游很坑的时候,
+ // 可以不限:你的下游很强,业务可用性要求很高,尽量容错策略
+ // 包一下这个错误
+ return fmt.Errorf("短信服务判断是否限流出现问题,%w", err)
+ }
+ if limited {
+ return errLimited
+ }
+ // 你这里加一些代码,新特性
+ err = s.Service.Send(ctx, tpl, args, numbers...)
+ // 你在这里也可以加一些代码,新特性
+ return err
+}
diff --git a/webook/internal/service/sms/retryable/service.go b/webook/internal/service/sms/retryable/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..8d5ad8394b8f7db86ac7a6b0dcf202fcc1ef7ea2
--- /dev/null
+++ b/webook/internal/service/sms/retryable/service.go
@@ -0,0 +1,51 @@
+package retryable
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+)
+
+// 这个要小心并发问题
+type Service struct {
+ svc sms.Service
+ // 重试
+ retryMax int
+}
+
+func NewService(svc sms.Service, retryMax int) sms.Service {
+ return &Service{
+ svc: svc,
+ retryMax: retryMax,
+ }
+}
+
+func (s Service) Send(ctx context.Context, tpl string, args []string, numbers ...string) error {
+ err := s.svc.Send(ctx, tpl, args, numbers...)
+ cnt := 1
+ for err != nil && cnt < s.retryMax {
+ err = s.svc.Send(ctx, tpl, args, numbers...)
+ if err == nil {
+ return nil
+ }
+ cnt++
+ }
+ return errors.New("重试都失败了")
+}
+
+// 设计并实现了一个高可用的短信平台
+// 1. 提高可用性:重试机制、客户端限流、failover(轮询,实时检测)
+// 1.1 实时检测:
+// 1.1.1 基于超时的实时检测(连续超时)
+// 1.1.2 基于响应时间的实时检测(比如说,平均响应时间上升 20%)
+// 1.1.3 基于长尾请求的实时检测(比如说,响应时间超过 1s 的请求占比超过了 10%)
+// 1.1.4 错误率
+// 2. 提高安全性:
+// 2.1 完整的资源申请与审批流程
+// 2.2 鉴权:
+// 2.2.1 静态 token
+// 2.2.2 动态 token
+// 3. 提高可观测性:日志、metrics, tracing,丰富完善的排查手段
+// 4. 提高性能,高性能:
+
+// 我没说怎么实现高并发
diff --git a/webook/internal/service/sms/tencent/service.go b/webook/internal/service/sms/tencent/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..51871fb7f7469cf0fef7d9816ca4ba722ef51625
--- /dev/null
+++ b/webook/internal/service/sms/tencent/service.go
@@ -0,0 +1,81 @@
+package tencent
+
+import (
+ "context"
+ "fmt"
+ mysms "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "gitee.com/geekbang/basic-go/webook/pkg/ratelimit"
+ "github.com/ecodeclub/ekit"
+ "github.com/ecodeclub/ekit/slice"
+ sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
+ "go.uber.org/zap"
+)
+
+type Service struct {
+ appId *string
+ signName *string
+ client *sms.Client
+ limiter ratelimit.Limiter
+}
+
+func NewService(client *sms.Client, appId string,
+ signName string, limiter ratelimit.Limiter) *Service {
+ return &Service{
+ client: client,
+ appId: ekit.ToPtr[string](appId),
+ signName: ekit.ToPtr[string](signName),
+ limiter: limiter,
+ }
+}
+
+// Send 一个是 []*string
+// 一个是 string,json 串
+// biz 直接代表的就是 tplId
+func (s *Service) Send(ctx context.Context,
+ biz string, args []string, numbers ...string) error {
+ req := sms.NewSendSmsRequest()
+ req.SmsSdkAppId = s.appId
+ req.SignName = s.signName
+ req.TemplateId = ekit.ToPtr[string](biz)
+ req.PhoneNumberSet = s.toStringPtrSlice(numbers)
+ req.TemplateParamSet = s.toStringPtrSlice(args)
+ resp, err := s.client.SendSms(req)
+ zap.L().Debug("发送短信", zap.Any("req", req),
+ zap.Any("resp", resp), zap.Error(err))
+ if err != nil {
+ return fmt.Errorf("腾讯短信服务发送失败 %w", err)
+ }
+ for _, status := range resp.Response.SendStatusSet {
+ if status.Code == nil || *(status.Code) != "Ok" {
+ return fmt.Errorf("发送短信失败 %s, %s ", *status.Code, *status.Message)
+ }
+ }
+ return nil
+}
+
+func (s *Service) SendV1(ctx context.Context, tplId string, args []mysms.NamedArg, numbers ...string) error {
+ req := sms.NewSendSmsRequest()
+ req.SmsSdkAppId = s.appId
+ req.SignName = s.signName
+ req.TemplateId = ekit.ToPtr[string](tplId)
+ req.PhoneNumberSet = s.toStringPtrSlice(numbers)
+ req.TemplateParamSet = slice.Map[mysms.NamedArg, *string](args, func(idx int, src mysms.NamedArg) *string {
+ return &src.Val
+ })
+ resp, err := s.client.SendSms(req)
+ if err != nil {
+ return err
+ }
+ for _, status := range resp.Response.SendStatusSet {
+ if status.Code == nil || *(status.Code) != "Ok" {
+ return fmt.Errorf("发送短信失败 %s, %s ", *status.Code, *status.Message)
+ }
+ }
+ return nil
+}
+
+func (s *Service) toStringPtrSlice(src []string) []*string {
+ return slice.Map[string, *string](src, func(idx int, src string) *string {
+ return &src
+ })
+}
diff --git a/webook/internal/service/sms/tencent/service_test.go b/webook/internal/service/sms/tencent/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8deb96959c4c329eec2381d320382e1606d90c94
--- /dev/null
+++ b/webook/internal/service/sms/tencent/service_test.go
@@ -0,0 +1,50 @@
+package tencent
+
+import (
+ "context"
+ "github.com/stretchr/testify/assert"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+ sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
+ "os"
+ "testing"
+)
+
+func TestSender(t *testing.T) {
+ secretId, ok := os.LookupEnv("SMS_SECRET_ID")
+ if !ok {
+ t.Fatal()
+ }
+ secretKey, ok := os.LookupEnv("SMS_SECRET_KEY")
+
+ c, err := sms.NewClient(common.NewCredential(secretId, secretKey),
+ "ap-nanjing",
+ profile.NewClientProfile())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := NewService(c, "1400842696", "妙影科技")
+
+ testCases := []struct {
+ name string
+ tplId string
+ params []string
+ numbers []string
+ wantErr error
+ }{
+ {
+ name: "发送验证码",
+ tplId: "1877556",
+ params: []string{"123456"},
+ // 改成你的手机号码
+ numbers: []string{"10086"},
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ er := s.Send(context.Background(), tc.tplId, tc.params, tc.numbers...)
+ assert.Equal(t, tc.wantErr, er)
+ })
+ }
+}
diff --git a/webook/internal/service/sms/tencent/service_v2.go b/webook/internal/service/sms/tencent/service_v2.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2f3ec140a99bc576b5fc3be0654a3c3f520bdef
--- /dev/null
+++ b/webook/internal/service/sms/tencent/service_v2.go
@@ -0,0 +1,67 @@
+package tencent
+
+import (
+ "context"
+ "fmt"
+ sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
+)
+
+type ServiceV1 struct {
+ client *sms.Client
+ appId *string
+ signName *string
+}
+
+func NewServiceV1(c *sms.Client, appId string, signName string) *Service {
+ return &Service{
+ client: c,
+ appId: toPtr[string](appId),
+ signName: toPtr[string](signName),
+ }
+}
+
+func (s *ServiceV1) Send(ctx context.Context, tplId string, args map[string]string, numbers ...string) error {
+ req := sms.NewSendSmsRequest()
+ req.PhoneNumberSet = toStringPtrSlice(numbers)
+ req.SmsSdkAppId = s.appId
+ // ctx 继续往下传
+ req.SetContext(ctx)
+ req.TemplateParamSet = mapToStringPtrSlice(args)
+ req.TemplateId = toPtr[string](tplId)
+ req.SignName = s.signName
+ resp, err := s.client.SendSms(req)
+ if err != nil {
+ return err
+ }
+ for _, status := range resp.Response.SendStatusSet {
+ if status.Code == nil || *(status.Code) != "Ok" {
+ return fmt.Errorf("发送失败,code: %s, 原因:%s",
+ *status.Code, *status.Message)
+ }
+ }
+ return nil
+}
+
+// string切片转换string 指针切片
+func toStringPtrSlice(src []string) []*string {
+ dst := make([]*string, len(src))
+ for i, s := range src {
+ dst[i] = &s
+ }
+ return dst
+}
+
+// map转换string 指针切片(当key没用的时候才用此方法)
+func mapToStringPtrSlice(src map[string]string) []*string {
+ dst := make([]*string, len(src))
+ var i int
+ for _, v := range src {
+ dst[i] = &v
+ i++
+ }
+ return dst
+}
+
+func toPtr[T any](t T) *T {
+ return &t
+}
diff --git a/webook/internal/service/sms/types.go b/webook/internal/service/sms/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..6958637f643b180e677efe7b966520214625dc09
--- /dev/null
+++ b/webook/internal/service/sms/types.go
@@ -0,0 +1,17 @@
+package sms
+
+import "context"
+
+type Service interface {
+ // Send biz 很含糊的业务
+ Send(ctx context.Context, biz string, args []string, numbers ...string) error
+ //SendV1(ctx context.Context, tpl string, args []NamedArg, numbers ...string) error
+ // 调用者需要知道实现者需要什么类型的参数,是 []string,还是 map[string]string
+ //SendV2(ctx context.Context, tpl string, args any, numbers ...string) error
+ //SendVV3(ctx context.Context, tpl string, args T, numbers ...string) error
+}
+
+type NamedArg struct {
+ Val string
+ Name string
+}
diff --git a/webook/internal/service/user.go b/webook/internal/service/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..f084006564f52e28bde2bffcf016bafb5dbb77b5
--- /dev/null
+++ b/webook/internal/service/user.go
@@ -0,0 +1,151 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "go.uber.org/zap"
+ "golang.org/x/crypto/bcrypt"
+)
+
+var ErrUserDuplicateEmail = repository.ErrUserDuplicate
+var ErrInvalidUserOrPassword = errors.New("账号/邮箱或密码不对")
+
+type UserService interface {
+ Login(ctx context.Context, email, password string) (domain.User, error)
+ SignUp(ctx context.Context, u domain.User) error
+ FindOrCreate(ctx context.Context, phone string) (domain.User, error)
+ FindOrCreateByWechat(ctx context.Context, wechatInfo domain.WechatInfo) (domain.User, error)
+ Profile(ctx context.Context, id int64) (domain.User, error)
+}
+
+type userService struct {
+ repo repository.UserRepository
+ l logger.LoggerV1
+}
+
+// NewUserService 我用的人,只管用,怎么初始化我不管,我一点都不关心如何初始化
+func NewUserService(repo repository.UserRepository, l logger.LoggerV1) UserService {
+ return &userService{
+ repo: repo,
+ l: l,
+ }
+}
+
+func NewUserServiceV1(repo repository.UserRepository, l *zap.Logger) UserService {
+ return &userService{
+ repo: repo,
+ // 预留了变化空间
+ //logger: zap.L(),
+ }
+}
+
+//func NewUserServiceV1(f repository.UserRepositoryFactory) UserService {
+// return &userService{
+// // 我在这里,不同的 factory,会创建出来不同实现
+// repo: f.NewRepo(),
+// }
+//}
+
+func (svc *userService) Login(ctx context.Context, email, password string) (domain.User, error) {
+ // 先找用户
+ u, err := svc.repo.FindByEmail(ctx, email)
+ if err == repository.ErrUserNotFound {
+ return domain.User{}, ErrInvalidUserOrPassword
+ }
+ if err != nil {
+ return domain.User{}, err
+ }
+ // 比较密码了
+ err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
+ if err != nil {
+ // DEBUG
+ return domain.User{}, ErrInvalidUserOrPassword
+ }
+
+ return u, nil
+}
+
+func (svc *userService) SignUp(ctx context.Context, u domain.User) error {
+ // 你要考虑加密放在哪里的问题了
+ hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ u.Password = string(hash)
+ // 然后就是,存起来
+ return svc.repo.Create(ctx, u)
+}
+
+func (svc *userService) FindOrCreate(ctx context.Context,
+ phone string) (domain.User, error) {
+ // 这时候,这个地方要怎么办?
+ // 这个叫做快路径
+ u, err := svc.repo.FindByPhone(ctx, phone)
+ // 要判断,有咩有这个用户
+ if err != repository.ErrUserNotFound {
+ // 绝大部分请求进来这里
+ // nil 会进来这里
+ // 不为 ErrUserNotFound 的也会进来这里
+ return u, err
+ }
+ // 这里,把 phone 脱敏之后打出来
+ //zap.L().Info("用户未注册", zap.String("phone", phone))
+ //svc.logger.Info("用户未注册", zap.String("phone", phone))
+ svc.l.Info("用户未注册", logger.String("phone", phone))
+ //loggerxx.Logger.Info("用户未注册", zap.String("phone", phone))
+ // 在系统资源不足,触发降级之后,不执行慢路径了
+ //if ctx.Value("降级") == "true" {
+ // return domain.User{}, errors.New("系统降级了")
+ //}
+ // 这个叫做慢路径
+ // 你明确知道,没有这个用户
+ u = domain.User{
+ Phone: phone,
+ }
+ err = svc.repo.Create(ctx, u)
+ if err != nil && err != repository.ErrUserDuplicate {
+ return u, err
+ }
+ // 因为这里会遇到主从延迟的问题
+ return svc.repo.FindByPhone(ctx, phone)
+}
+
+func (svc *userService) FindOrCreateByWechat(ctx context.Context,
+ info domain.WechatInfo) (domain.User, error) {
+ // 像这种
+ u, err := svc.repo.FindByWechat(ctx, info.OpenID)
+ if err != repository.ErrUserNotFound {
+ return u, err
+ }
+ u = domain.User{
+ WechatInfo: info,
+ }
+ // 所谓的慢路径
+ // 你是不是可以说,在降级、限流、熔断的时候,就禁止注册
+ if ctx.Value("limited") == "true" {
+ return domain.User{}, errors.New("触发限流,禁用注册")
+ }
+ err = svc.repo.Create(ctx, u)
+ if err != nil && err != repository.ErrUserDuplicate {
+ return u, err
+ }
+ // 因为这里会遇到主从延迟的问题
+ return svc.repo.FindByWechat(ctx, info.OpenID)
+}
+
+func (svc *userService) Profile(ctx context.Context,
+ id int64) (domain.User, error) {
+ u, err := svc.repo.FindById(ctx, id)
+ return u, err
+}
+
+func PathsDownGrade(ctx context.Context, quick, slow func()) {
+ quick()
+ if ctx.Value("降级") == "true" {
+ return
+ }
+ slow()
+}
diff --git a/webook/internal/service/user_test.go b/webook/internal/service/user_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..87b8036a1bc72528c0ee5d0487739353a9f16d35
--- /dev/null
+++ b/webook/internal/service/user_test.go
@@ -0,0 +1,125 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ repomocks "gitee.com/geekbang/basic-go/webook/internal/repository/mocks"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "golang.org/x/crypto/bcrypt"
+ "testing"
+ "time"
+)
+
+func Test_userService_Login(t *testing.T) {
+ // 做成一个测试用例都用到的时间
+ now := time.Now()
+
+ testCases := []struct {
+ name string
+ mock func(ctrl *gomock.Controller) repository.UserRepository
+
+ // 输入
+ //ctx context.Context
+ email string
+ password string
+
+ // 输出
+ wantUser domain.User
+ wantErr error
+ }{
+ {
+ name: "登录成功", // 用户名和密码是对的
+ mock: func(ctrl *gomock.Controller) repository.UserRepository {
+ repo := repomocks.NewMockUserRepository(ctrl)
+ repo.EXPECT().FindByEmail(gomock.Any(), "123@qq.com").
+ Return(domain.User{
+ Email: "123@qq.com",
+ Password: "$2a$10$MN9ZKKIbjLZDyEpCYW19auY7mvOG9pcpiIcUUoZZI6pA6OmKZKOVi",
+ Phone: "15212345678",
+ Ctime: now,
+ }, nil)
+ return repo
+ },
+ email: "123@qq.com",
+ password: "hello#world123",
+
+ wantUser: domain.User{
+ Email: "123@qq.com",
+ Password: "$2a$10$MN9ZKKIbjLZDyEpCYW19auY7mvOG9pcpiIcUUoZZI6pA6OmKZKOVi",
+ Phone: "15212345678",
+ Ctime: now,
+ },
+ wantErr: nil,
+ },
+ {
+ name: "用户不存在",
+ mock: func(ctrl *gomock.Controller) repository.UserRepository {
+ repo := repomocks.NewMockUserRepository(ctrl)
+ repo.EXPECT().FindByEmail(gomock.Any(), "123@qq.com").
+ Return(domain.User{}, repository.ErrUserNotFound)
+ return repo
+ },
+ email: "123@qq.com",
+ password: "hello#world123",
+
+ wantUser: domain.User{},
+ wantErr: ErrInvalidUserOrPassword,
+ },
+ {
+ name: "DB错误",
+ mock: func(ctrl *gomock.Controller) repository.UserRepository {
+ repo := repomocks.NewMockUserRepository(ctrl)
+ repo.EXPECT().FindByEmail(gomock.Any(), "123@qq.com").
+ Return(domain.User{}, errors.New("mock db 错误"))
+ return repo
+ },
+ email: "123@qq.com",
+ password: "hello#world123",
+
+ wantUser: domain.User{},
+ wantErr: errors.New("mock db 错误"),
+ },
+ {
+ name: "密码不对",
+ mock: func(ctrl *gomock.Controller) repository.UserRepository {
+ repo := repomocks.NewMockUserRepository(ctrl)
+ repo.EXPECT().FindByEmail(gomock.Any(), "123@qq.com").
+ Return(domain.User{
+ Email: "123@qq.com",
+ Password: "$2a$10$MN9ZKKIbjLZDyEpCYW19auY7mvOG9pcpiIcUUoZZI6pA6OmKZKOVi",
+ Phone: "15212345678",
+ Ctime: now,
+ }, nil)
+ return repo
+ },
+ email: "123@qq.com",
+ password: "112443rsdffhello#world123",
+
+ wantUser: domain.User{},
+ wantErr: ErrInvalidUserOrPassword,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ // 具体的测试代码
+ svc := NewUserService(tc.mock(ctrl), &logger.NopLogger{})
+ u, err := svc.Login(context.Background(), tc.email, tc.password)
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantUser, u)
+ })
+ }
+}
+
+func TestEncrypted(t *testing.T) {
+ res, err := bcrypt.GenerateFromPassword([]byte("hello#world123"), bcrypt.DefaultCost)
+ if err == nil {
+ t.Log(string(res))
+ }
+}
diff --git a/webook/internal/web/article.go b/webook/internal/web/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4528eb65cf968413e65d6a7ff6dc3b4ec74aaf3
--- /dev/null
+++ b/webook/internal/web/article.go
@@ -0,0 +1,418 @@
+package web
+
+import (
+ "fmt"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ rewardv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/reward/v1"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/ecodeclub/ekit/slice"
+ "github.com/gin-gonic/gin"
+ "golang.org/x/sync/errgroup"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+var _ handler = (*ArticleHandler)(nil)
+
+type ArticleHandler struct {
+ svc service.ArticleService
+ l logger.LoggerV1
+ intrSvc intrv1.InteractiveServiceClient
+ rewardSvc rewardv1.RewardServiceClient
+ biz string
+}
+
+func NewArticleHandler(svc service.ArticleService,
+ // 你可以注入真的 grpc 客户端,也可以注入那个本地调用伪装的。
+ intrSvc intrv1.InteractiveServiceClient,
+ l logger.LoggerV1) *ArticleHandler {
+ return &ArticleHandler{
+ svc: svc,
+ l: l,
+ biz: "article",
+ intrSvc: intrSvc,
+ }
+}
+
+//type ArticleHandlerV2 struct {
+// ArticleHandler
+//}
+//
+//func (a *ArticleHandlerV2) Like(ctx *gin.Context, req LikeReq, uc ijwt.UserClaims) (ginx.Result, error) {
+// // 重写
+//}
+//
+//func (h *ArticleHandlerV2) RegisterRoutes(server *gin.Engine) {
+// v1 := server.Group("/api/v2")
+//}
+
+func (h *ArticleHandler) RegisterRoutes(server *gin.Engine) {
+ //v1 := server.Group("/api/v1")
+ //g := v1.Group("/articles")
+ g := server.Group("/articles")
+ // 修改
+ //g.PUT("/")
+ // 新增
+ //g.POST("/")
+ // g.DELETE("/a_id")
+
+ g.POST("/edit", h.Edit)
+ g.POST("/withdraw", h.Withdraw)
+ g.POST("/publish", h.Publish)
+ // 创作者的查询接口
+ // 这个是获取数据的接口,理论上来说(遵循 RESTful 规范),应该是用 GET 方法
+ // GET localhost/articles => List 接口
+ g.POST("/list",
+ ginx.WrapBodyAndToken[ListReq, ijwt.UserClaims](h.List))
+ g.GET("/detail/:id", ginx.WrapToken[ijwt.UserClaims](h.Detail))
+
+ pub := g.Group("/pub")
+ pub.GET("/:id", h.PubDetail, func(ctx *gin.Context) {
+ // 增加阅读计数。
+ //go func() {
+ // // 开一个 goroutine,异步去执行
+ // er := a.intrSvc.IncrReadCnt(ctx, a.biz, art.Id)
+ // if er != nil {
+ // a.l.Error("增加阅读计数失败",
+ // logger.Int64("aid", art.Id),
+ // logger.Error(err))
+ // }
+ //}()
+ })
+ // 点赞是这个接口,取消点赞也是这个接口
+ // RESTful 风格
+ //pub.POST("/like/:id", ginx.WrapBodyAndToken[LikeReq,
+ // ijwt.UserClaims](h.Like))
+ pub.POST("/like", ginx.WrapBodyAndToken[LikeReq,
+ ijwt.UserClaims](h.Like))
+ //pub.POST("/cancel_like", ginx.WrapBodyAndToken[LikeReq,
+ // ijwt.UserClaims](h.Like))
+ // 在这里定义 打赏接口
+ pub.POST("/reward", ginx.WrapBodyAndToken[RewardReq,
+ ijwt.UserClaims](h.Reward))
+}
+
+func (a *ArticleHandler) Like(ctx *gin.Context, req LikeReq, uc ijwt.UserClaims) (ginx.Result, error) {
+ var err error
+ if req.Like {
+ _, err = a.intrSvc.Like(ctx, &intrv1.LikeRequest{
+ Biz: a.biz, BizId: req.Id, Uid: uc.Id,
+ })
+ } else {
+ _, err = a.intrSvc.CancelLike(ctx, &intrv1.CancelLikeRequest{
+ Biz: a.biz, BizId: req.Id, Uid: uc.Id,
+ })
+ }
+
+ if err != nil {
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统错误",
+ }, err
+ }
+ return ginx.Result{Msg: "OK"}, nil
+}
+
+func (a *ArticleHandler) PubDetail(ctx *gin.Context) {
+ idstr := ctx.Param("id")
+ id, err := strconv.ParseInt(idstr, 10, 64)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 4,
+ Msg: "参数错误",
+ })
+ a.l.Error("前端输入的 ID 不对", logger.Error(err))
+ return
+ }
+
+ uc := ctx.MustGet("users").(ijwt.UserClaims)
+ var eg errgroup.Group
+ var art domain.Article
+ eg.Go(func() error {
+ art, err = a.svc.GetPublishedById(ctx, id, uc.Id)
+ return err
+ })
+
+ var getResp *intrv1.GetResponse
+ eg.Go(func() error {
+ // 这个地方可以容忍错误
+ getResp, err = a.intrSvc.Get(ctx, &intrv1.GetRequest{
+ Biz: a.biz, BizId: id, Uid: uc.Id,
+ })
+ // 这种是容错的写法
+ //if err != nil {
+ // // 记录日志
+ //}
+ //return nil
+ return err
+ })
+
+ // 在这儿等,要保证前面两个
+ err = eg.Wait()
+ if err != nil {
+ // 代表查询出错了
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+
+ // 增加阅读计数。
+ go func() {
+ // 你都异步了,怎么还说有巨大的压力呢?
+ // 开一个 goroutine,异步去执行
+ _, er := a.intrSvc.IncrReadCnt(ctx, &intrv1.IncrReadCntRequest{
+ Biz: a.biz, BizId: art.Id,
+ })
+ if er != nil {
+ a.l.Error("增加阅读计数失败",
+ logger.Int64("aid", art.Id),
+ logger.Error(err))
+ }
+ }()
+
+ // ctx.Set("art", art)
+ intr := getResp.Intr
+
+ // 这个功能是不是可以让前端,主动发一个 HTTP 请求,来增加一个计数?
+ ctx.JSON(http.StatusOK, Result{
+ Data: ArticleVO{
+ Id: art.Id,
+ Title: art.Title,
+ Status: art.Status.ToUint8(),
+ Content: art.Content,
+ // 要把作者信息带出去
+ Author: art.Author.Name,
+ Ctime: art.Ctime.Format(time.DateTime),
+ Utime: art.Utime.Format(time.DateTime),
+ Liked: intr.Liked,
+ Collected: intr.Collected,
+ LikeCnt: intr.LikeCnt,
+ ReadCnt: intr.ReadCnt,
+ CollectCnt: intr.CollectCnt,
+ },
+ })
+}
+
+func (a *ArticleHandler) Detail(ctx *gin.Context, usr ijwt.UserClaims) (ginx.Result, error) {
+ idstr := ctx.Param("id")
+ id, err := strconv.ParseInt(idstr, 10, 64)
+ if err != nil {
+ //ctx.JSON(http.StatusOK, )
+ //a.l.Error("前端输入的 ID 不对", logger.Error(err))
+ return ginx.Result{
+ Code: 4,
+ Msg: "参数错误",
+ }, err
+ }
+ art, err := a.svc.GetById(ctx, id)
+ if err != nil {
+ //ctx.JSON(http.StatusOK, )
+ //a.l.Error("获得文章信息失败", logger.Error(err))
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统错误",
+ }, err
+ }
+ // 这是不借助数据库查询来判定的方法
+ if art.Author.Id != usr.Id {
+ //ctx.JSON(http.StatusOK)
+ // 如果公司有风控系统,这个时候就要上报这种非法访问的用户了。
+ //a.l.Error("非法访问文章,创作者 ID 不匹配",
+ // logger.Int64("uid", usr.Id))
+ return ginx.Result{
+ Code: 4,
+ // 也不需要告诉前端究竟发生了什么
+ Msg: "输入有误",
+ }, fmt.Errorf("非法访问文章,创作者 ID 不匹配 %d", usr.Id)
+ }
+ return ginx.Result{
+ Data: ArticleVO{
+ Id: art.Id,
+ Title: art.Title,
+ // 不需要这个摘要信息
+ //Abstract: art.Abstract(),
+ Status: art.Status.ToUint8(),
+ Content: art.Content,
+ // 这个是创作者看自己的文章列表,也不需要这个字段
+ //Author: art.Author
+ Ctime: art.Ctime.Format(time.DateTime),
+ Utime: art.Utime.Format(time.DateTime),
+ },
+ }, nil
+}
+
+func (h *ArticleHandler) Publish(ctx *gin.Context) {
+ var req ArticleReq
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ c := ctx.MustGet("users")
+ claims, ok := c.(ijwt.UserClaims)
+ if !ok {
+ // 你可以考虑监控住这里
+ //ctx.AbortWithStatus(http.StatusUnauthorized)
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ h.l.Error("未发现用户的 session 信息")
+ return
+ }
+
+ id, err := h.svc.Publish(ctx, req.toDomain(claims.Id))
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ // 打日志?
+ h.l.Error("发表帖子失败", logger.Error(err))
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Data: id,
+ })
+}
+
+func (h *ArticleHandler) Withdraw(ctx *gin.Context) {
+ type Req struct {
+ Id int64
+ }
+ var req Req
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ c := ctx.MustGet("users")
+ claims, ok := c.(ijwt.UserClaims)
+ if !ok {
+ // 你可以考虑监控住这里
+ //ctx.AbortWithStatus(http.StatusUnauthorized)
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ h.l.Error("未发现用户的 session 信息")
+ return
+ }
+
+ // 检测输入,跳过这一步
+ // 调用 svc 的代码
+ err := h.svc.Withdraw(ctx, domain.Article{
+ Id: req.Id,
+ Author: domain.Author{
+ Id: claims.Id,
+ },
+ })
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ // 打日志?
+ h.l.Error("保存帖子失败", logger.Error(err))
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "OK",
+ })
+}
+
+func (h *ArticleHandler) Edit(ctx *gin.Context) {
+ var req ArticleReq
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ c := ctx.MustGet("users")
+ claims, ok := c.(ijwt.UserClaims)
+ if !ok {
+ // 你可以考虑监控住这里
+ //ctx.AbortWithStatus(http.StatusUnauthorized)
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ h.l.Error("未发现用户的 session 信息")
+ return
+ }
+ // 检测输入,跳过这一步
+ // 调用 svc 的代码
+ id, err := h.svc.Save(ctx, req.toDomain(claims.Id))
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ // 打日志?
+ h.l.Error("保存帖子失败", logger.Error(err))
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Data: id,
+ })
+}
+
+func (h *ArticleHandler) List(ctx *gin.Context, req ListReq, uc ijwt.UserClaims) (ginx.Result, error) {
+ res, err := h.svc.List(ctx, uc.Id, req.Offset, req.Limit)
+ if err != nil {
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统错误",
+ }, nil
+ }
+ // 在列表页,不显示全文,只显示一个"摘要"
+ // 比如说,简单的摘要就是前几句话
+ // 强大的摘要是 AI 帮你生成的
+ return ginx.Result{
+ Data: slice.Map[domain.Article, ArticleVO](res,
+ func(idx int, src domain.Article) ArticleVO {
+ return ArticleVO{
+ Id: src.Id,
+ Title: src.Title,
+ Abstract: src.Abstract(),
+ Status: src.Status.ToUint8(),
+ // 这个列表请求,不需要返回内容
+ //Content: src.Content,
+ // 这个是创作者看自己的文章列表,也不需要这个字段
+ //Author: src.Author
+ Ctime: src.Ctime.Format(time.DateTime),
+ Utime: src.Utime.Format(time.DateTime),
+ }
+ }),
+ }, nil
+}
+
+func (h *ArticleHandler) Reward(ctx *gin.Context, req RewardReq, uc ijwt.UserClaims) (ginx.Result, error) {
+ art, err := h.svc.GetPublishedById(ctx, req.Id, uc.Id)
+ if err != nil {
+ return ginx.Result{}, err
+ }
+ // 我要在这里实现打赏
+ // 拿到一个打赏的二维码
+ // 我不是直接调用支付,而是调用打赏
+ // 打赏什么东西,谁打赏,打赏多少钱?
+ resp, err := h.rewardSvc.PreReward(ctx, &rewardv1.PreRewardRequest{
+ Biz: "article",
+ BizId: req.Id,
+ Uid: uc.Id,
+ Amt: req.Amount,
+ // 创作者是谁?
+ TargetUid: art.Author.Id,
+ // 这个地方用作者呢?还是用标题呢?
+ // 作者写得好
+ BizName: art.Title,
+ })
+ return ginx.Result{
+ Data: map[string]any{
+ "codeURL": resp.CodeUrl,
+ // 代表的是这一次的打赏
+ "rid": resp.Rid,
+ },
+ }, nil
+}
diff --git a/webook/internal/web/article_test.go b/webook/internal/web/article_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..48caf9a2e7489265e1d8786a973336aa28f09f84
--- /dev/null
+++ b/webook/internal/web/article_test.go
@@ -0,0 +1,114 @@
+package web
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ svcmocks "gitee.com/geekbang/basic-go/webook/internal/service/mocks"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestArticleHandler_Publish(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ mock func(ctrl *gomock.Controller) service.ArticleService
+
+ reqBody string
+
+ wantCode int
+ wantRes Result
+ }{
+ {
+ name: "新建并发表",
+ mock: func(ctrl *gomock.Controller) service.ArticleService {
+ svc := svcmocks.NewMockArticleService(ctrl)
+ svc.EXPECT().Publish(gomock.Any(), domain.Article{
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(1), nil)
+ return svc
+ },
+ reqBody: `
+{
+ "title":"我的标题",
+ "content": "我的内容"
+}
+`,
+ wantCode: 200,
+ wantRes: Result{
+ Data: float64(1),
+ Msg: "OK",
+ },
+ },
+ {
+ name: "publish失败",
+ mock: func(ctrl *gomock.Controller) service.ArticleService {
+ svc := svcmocks.NewMockArticleService(ctrl)
+ svc.EXPECT().Publish(gomock.Any(), domain.Article{
+ Title: "我的标题",
+ Content: "我的内容",
+ Author: domain.Author{
+ Id: 123,
+ },
+ }).Return(int64(0), errors.New("publish error"))
+ return svc
+ },
+ reqBody: `
+{
+ "title":"我的标题",
+ "content": "我的内容"
+}
+`,
+ wantCode: 200,
+ wantRes: Result{
+ Code: 5,
+ Msg: "系统错误",
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ server := gin.Default()
+ server.Use(func(ctx *gin.Context) {
+ ctx.Set("users", &ijwt.UserClaims{
+ Id: 123,
+ })
+ })
+ // 用不上 codeSvc
+ h := NewArticleHandler(tc.mock(ctrl), &logger.NopLogger{})
+ h.RegisterRoutes(server)
+
+ req, err := http.NewRequest(http.MethodPost,
+ "/articles/publish", bytes.NewBuffer([]byte(tc.reqBody)))
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/json")
+ resp := httptest.NewRecorder()
+ server.ServeHTTP(resp, req)
+
+ assert.Equal(t, tc.wantCode, resp.Code)
+ if resp.Code != 200 {
+ return
+ }
+ var webRes Result
+ err = json.NewDecoder(resp.Body).Decode(&webRes)
+ require.NoError(t, err)
+ assert.Equal(t, tc.wantRes, webRes)
+ })
+ }
+}
diff --git a/webook/internal/web/article_vo.go b/webook/internal/web/article_vo.go
new file mode 100644
index 0000000000000000000000000000000000000000..56e8754c66c378069d5903f8ed92939ea4bb3a1f
--- /dev/null
+++ b/webook/internal/web/article_vo.go
@@ -0,0 +1,64 @@
+package web
+
+import "gitee.com/geekbang/basic-go/webook/internal/domain"
+
+type RewardReq struct {
+ Id int64 `json:"id"`
+ Amount int64 `json:"amount"`
+}
+
+// VO view object,就是对标前端的
+
+type LikeReq struct {
+ Id int64 `json:"id"`
+ // 点赞和取消点赞,我都准备复用这个
+ Like bool `json:"like"`
+}
+
+type ArticleVO struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ // 摘要
+ Abstract string `json:"abstract"`
+ // 内容
+ Content string `json:"content"`
+ // 注意一点,状态这个东西,可以是前端来处理,也可以是后端处理
+ // 0 -> unknown -> 未知状态
+ // 1 -> 未发表,手机 APP 这种涉及到发版的问题,那么后端来处理
+ // 涉及到国际化,也是后端来处理
+ Status uint8 `json:"status"`
+ Author string `json:"author"`
+ // 计数
+ ReadCnt int64 `json:"read_cnt"`
+ LikeCnt int64 `json:"like_cnt"`
+ CollectCnt int64 `json:"collect_cnt"`
+
+ // 我个人有没有收藏,有没有点赞
+ Liked bool `json:"liked"`
+ Collected bool `json:"collected"`
+
+ Ctime string `json:"ctime"`
+ Utime string `json:"utime"`
+}
+
+type ListReq struct {
+ Offset int `json:"offset"`
+ Limit int `json:"limit"`
+}
+
+type ArticleReq struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+}
+
+func (req ArticleReq) toDomain(uid int64) domain.Article {
+ return domain.Article{
+ Id: req.Id,
+ Title: req.Title,
+ Content: req.Content,
+ Author: domain.Author{
+ Id: uid,
+ },
+ }
+}
diff --git a/webook/internal/web/client/grey_scale_intr.go b/webook/internal/web/client/grey_scale_intr.go
new file mode 100644
index 0000000000000000000000000000000000000000..48a4b884562f06de694bdc08d2216b96d8497d1d
--- /dev/null
+++ b/webook/internal/web/client/grey_scale_intr.go
@@ -0,0 +1,92 @@
+package client
+
+import (
+ "context"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "google.golang.org/grpc"
+ "math/rand"
+)
+
+type GreyScaleInteractiveServiceClient struct {
+ remote intrv1.InteractiveServiceClient
+ local intrv1.InteractiveServiceClient
+ // 我怎么控制流量呢?
+ // 如果一个请求过来,我该怎么控制它去调用本地,还是调用远程呢?
+ // 用随机数 + 阈值的小技巧
+ threshold *atomicx.Value[int32]
+}
+
+func NewGreyScaleInteractiveServiceClient(remote intrv1.InteractiveServiceClient, local intrv1.InteractiveServiceClient) *GreyScaleInteractiveServiceClient {
+ return &GreyScaleInteractiveServiceClient{
+ remote: remote,
+ local: local,
+ threshold: atomicx.NewValue[int32](),
+ }
+}
+
+// StartListen 这种做法的缺陷是,GreyScaleInteractiveServiceClient 和 viper 紧耦合
+//func (g *GreyScaleInteractiveServiceClient) StartListen() error {
+// viper.OnConfigChange(func(in fsnotify.Event) {
+//
+// })
+//}
+
+func (g *GreyScaleInteractiveServiceClient) OnChange(ch <-chan int32) {
+ go func() {
+ for newTh := range ch {
+ g.threshold.Store(newTh)
+ }
+ }()
+}
+
+func (g *GreyScaleInteractiveServiceClient) OnChangeV1() chan<- int32 {
+ ch := make(chan int32, 100)
+ go func() {
+ for newTh := range ch {
+ g.threshold.Store(newTh)
+ }
+ }()
+ return ch
+}
+
+func (g *GreyScaleInteractiveServiceClient) IncrReadCnt(ctx context.Context, in *intrv1.IncrReadCntRequest, opts ...grpc.CallOption) (*intrv1.IncrReadCntResponse, error) {
+ return g.client().IncrReadCnt(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) Like(ctx context.Context, in *intrv1.LikeRequest, opts ...grpc.CallOption) (*intrv1.LikeResponse, error) {
+ return g.client().Like(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) CancelLike(ctx context.Context, in *intrv1.CancelLikeRequest, opts ...grpc.CallOption) (*intrv1.CancelLikeResponse, error) {
+ return g.client().CancelLike(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) Collect(ctx context.Context, in *intrv1.CollectRequest, opts ...grpc.CallOption) (*intrv1.CollectResponse, error) {
+ return g.client().Collect(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) Get(ctx context.Context, in *intrv1.GetRequest, opts ...grpc.CallOption) (*intrv1.GetResponse, error) {
+ return g.client().Get(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) GetByIds(ctx context.Context, in *intrv1.GetByIdsRequest, opts ...grpc.CallOption) (*intrv1.GetByIdsResponse, error) {
+ return g.client().GetByIds(ctx, in, opts...)
+}
+
+func (g *GreyScaleInteractiveServiceClient) UpdateThreshold(newThreshold int32) {
+ g.threshold.Store(newThreshold)
+}
+
+func (g *GreyScaleInteractiveServiceClient) client() intrv1.InteractiveServiceClient {
+ threshold := g.threshold.Load()
+ // [0-100)的随机数
+ num := rand.Int31n(100)
+ // 举例来说,如果要是 threshold 是 100,
+ // 可以预见的是,所有的 num 都会进去,返回 remote
+ if num < threshold {
+ return g.remote
+ }
+ // 假如说我的 threshold 是0,那么就会永远用本地的
+ return g.local
+}
diff --git a/webook/internal/web/client/intr_local.go b/webook/internal/web/client/intr_local.go
new file mode 100644
index 0000000000000000000000000000000000000000..59d201a06a35cf6acb48b00acf997db7184f1295
--- /dev/null
+++ b/webook/internal/web/client/intr_local.go
@@ -0,0 +1,75 @@
+package client
+
+import (
+ "context"
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ domain2 "gitee.com/geekbang/basic-go/webook/interactive/domain"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "google.golang.org/grpc"
+)
+
+// InteractiveServiceAdapter 将一个本地实现伪装成一个 gRPC 客户端
+type InteractiveServiceAdapter struct {
+ svc service.InteractiveService
+}
+
+func NewInteractiveServiceAdapter(svc service.InteractiveService) *InteractiveServiceAdapter {
+ return &InteractiveServiceAdapter{svc: svc}
+}
+
+func (i *InteractiveServiceAdapter) IncrReadCnt(ctx context.Context, in *intrv1.IncrReadCntRequest, opts ...grpc.CallOption) (*intrv1.IncrReadCntResponse, error) {
+ err := i.svc.IncrReadCnt(ctx, in.GetBiz(), in.GetBizId())
+ return &intrv1.IncrReadCntResponse{}, err
+}
+
+func (i *InteractiveServiceAdapter) Like(ctx context.Context, in *intrv1.LikeRequest, opts ...grpc.CallOption) (*intrv1.LikeResponse, error) {
+ err := i.svc.Like(ctx, in.GetBiz(), in.GetBizId(), in.GetUid())
+ return &intrv1.LikeResponse{}, err
+}
+
+func (i *InteractiveServiceAdapter) CancelLike(ctx context.Context, in *intrv1.CancelLikeRequest, opts ...grpc.CallOption) (*intrv1.CancelLikeResponse, error) {
+ err := i.svc.CancelLike(ctx, in.GetBiz(), in.GetBizId(), in.GetUid())
+ return &intrv1.CancelLikeResponse{}, err
+}
+
+func (i *InteractiveServiceAdapter) Collect(ctx context.Context, in *intrv1.CollectRequest, opts ...grpc.CallOption) (*intrv1.CollectResponse, error) {
+ err := i.svc.Collect(ctx, in.GetBiz(), in.GetBizId(), in.GetUid(), in.GetCid())
+ return &intrv1.CollectResponse{}, err
+}
+
+func (i *InteractiveServiceAdapter) Get(ctx context.Context, in *intrv1.GetRequest, opts ...grpc.CallOption) (*intrv1.GetResponse, error) {
+ intr, err := i.svc.Get(ctx, in.GetBiz(), in.GetBizId(), in.GetUid())
+ if err != nil {
+ return nil, err
+ }
+ return &intrv1.GetResponse{
+ Intr: i.toDTO(intr),
+ }, nil
+}
+
+func (i *InteractiveServiceAdapter) GetByIds(ctx context.Context, in *intrv1.GetByIdsRequest, opts ...grpc.CallOption) (*intrv1.GetByIdsResponse, error) {
+ res, err := i.svc.GetByIds(ctx, in.GetBiz(), in.GetIds())
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[int64]*intrv1.Interactive, len(res))
+ for k, v := range res {
+ m[k] = i.toDTO(v)
+ }
+ return &intrv1.GetByIdsResponse{
+ Intrs: m,
+ }, nil
+}
+
+// DTO data transfer object
+func (i *InteractiveServiceAdapter) toDTO(intr domain2.Interactive) *intrv1.Interactive {
+ return &intrv1.Interactive{
+ Biz: intr.Biz,
+ BizId: intr.BizId,
+ CollectCnt: intr.CollectCnt,
+ Collected: intr.Collected,
+ LikeCnt: intr.LikeCnt,
+ Liked: intr.Liked,
+ ReadCnt: intr.ReadCnt,
+ }
+}
diff --git a/webook/internal/web/consts.go b/webook/internal/web/consts.go
new file mode 100644
index 0000000000000000000000000000000000000000..efb38952601b8c71ceecfbd7be39ea96f70c53c6
--- /dev/null
+++ b/webook/internal/web/consts.go
@@ -0,0 +1 @@
+package web
diff --git a/webook/internal/web/init_web.go b/webook/internal/web/init_web.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ec8e762e7529ed6faa7372e0ec2b12eb74fa7de
--- /dev/null
+++ b/webook/internal/web/init_web.go
@@ -0,0 +1,34 @@
+package web
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+func RegisterRoutes() *gin.Engine {
+ server := gin.Default()
+ registerUsersRoutes(server)
+ return server
+}
+
+func registerUsersRoutes(server *gin.Engine) {
+ u := &UserHandler{}
+ server.POST("/users/signup", u.SignUp)
+ // 这是 REST 风格
+ //server.PUT("/user", func(context *gin.Context) {
+ //
+ //})
+
+ server.POST("/users/login", u.Login)
+
+ server.POST("/users/edit", u.Edit)
+ // REST 风格
+ //server.POST("/users/:id", func(context *gin.Context) {
+ //
+ //})
+
+ server.GET("/users/profile", u.Profile)
+ // REST 风格
+ //server.GET("/users/:id", func(context *gin.Context) {
+ //
+ //})
+}
diff --git a/webook/internal/web/jwt/mocks/handler.mock.go b/webook/internal/web/jwt/mocks/handler.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..c15ce8ec8c0407e56ad0f1bf5e6c329d2e2e61f3
--- /dev/null
+++ b/webook/internal/web/jwt/mocks/handler.mock.go
@@ -0,0 +1,105 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./webook/internal/web/jwt/types.go
+
+// Package jwtmocks is a generated GoMock package.
+package jwtmocks
+
+import (
+ reflect "reflect"
+
+ gin "github.com/gin-gonic/gin"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockHandler is a mock of Handler interface.
+type MockHandler struct {
+ ctrl *gomock.Controller
+ recorder *MockHandlerMockRecorder
+}
+
+// MockHandlerMockRecorder is the mock recorder for MockHandler.
+type MockHandlerMockRecorder struct {
+ mock *MockHandler
+}
+
+// NewMockHandler creates a new mock instance.
+func NewMockHandler(ctrl *gomock.Controller) *MockHandler {
+ mock := &MockHandler{ctrl: ctrl}
+ mock.recorder = &MockHandlerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockHandler) EXPECT() *MockHandlerMockRecorder {
+ return m.recorder
+}
+
+// CheckSession mocks base method.
+func (m *MockHandler) CheckSession(ctx *gin.Context, ssid string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CheckSession", ctx, ssid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CheckSession indicates an expected call of CheckSession.
+func (mr *MockHandlerMockRecorder) CheckSession(ctx, ssid interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckSession", reflect.TypeOf((*MockHandler)(nil).CheckSession), ctx, ssid)
+}
+
+// ClearToken mocks base method.
+func (m *MockHandler) ClearToken(ctx *gin.Context) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ClearToken", ctx)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ClearToken indicates an expected call of ClearToken.
+func (mr *MockHandlerMockRecorder) ClearToken(ctx interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearToken", reflect.TypeOf((*MockHandler)(nil).ClearToken), ctx)
+}
+
+// ExtractTokenString mocks base method.
+func (m *MockHandler) ExtractTokenString(ctx *gin.Context) string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExtractTokenString", ctx)
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// ExtractTokenString indicates an expected call of ExtractTokenString.
+func (mr *MockHandlerMockRecorder) ExtractTokenString(ctx interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtractTokenString", reflect.TypeOf((*MockHandler)(nil).ExtractTokenString), ctx)
+}
+
+// SetJWTToken mocks base method.
+func (m *MockHandler) SetJWTToken(ctx *gin.Context, ssid string, uid int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetJWTToken", ctx, ssid, uid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SetJWTToken indicates an expected call of SetJWTToken.
+func (mr *MockHandlerMockRecorder) SetJWTToken(ctx, ssid, uid interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetJWTToken", reflect.TypeOf((*MockHandler)(nil).SetJWTToken), ctx, ssid, uid)
+}
+
+// SetLoginToken mocks base method.
+func (m *MockHandler) SetLoginToken(ctx *gin.Context, uid int64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetLoginToken", ctx, uid)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SetLoginToken indicates an expected call of SetLoginToken.
+func (mr *MockHandlerMockRecorder) SetLoginToken(ctx, uid interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLoginToken", reflect.TypeOf((*MockHandler)(nil).SetLoginToken), ctx, uid)
+}
diff --git a/webook/internal/web/jwt/redis_jwt.go b/webook/internal/web/jwt/redis_jwt.go
new file mode 100644
index 0000000000000000000000000000000000000000..9043aa0bcd0c04686a607905a87af16b3a741695
--- /dev/null
+++ b/webook/internal/web/jwt/redis_jwt.go
@@ -0,0 +1,107 @@
+package jwt
+
+import (
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/google/uuid"
+ "github.com/redis/go-redis/v9"
+ "strings"
+ "time"
+)
+
+var (
+ AtKey = []byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0")
+ RtKey = []byte("95osj3fUD7fo0mlYdDbncXz4VD2igvfx")
+)
+
+type RedisJWTHandler struct {
+ cmd redis.Cmdable
+}
+
+func NewRedisJWTHandler(cmd redis.Cmdable) Handler {
+ return &RedisJWTHandler{
+ cmd: cmd,
+ }
+}
+
+func (h *RedisJWTHandler) SetLoginToken(ctx *gin.Context, uid int64) error {
+ ssid := uuid.New().String()
+ err := h.SetJWTToken(ctx, uid, ssid)
+ if err != nil {
+ return err
+ }
+ err = h.setRefreshToken(ctx, uid, ssid)
+ return err
+}
+
+func (h *RedisJWTHandler) setRefreshToken(ctx *gin.Context, uid int64, ssid string) error {
+ claims := RefreshClaims{
+ Ssid: ssid,
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)),
+ },
+ Uid: uid,
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
+ tokenStr, err := token.SignedString(RtKey)
+ if err != nil {
+ return err
+ }
+ ctx.Header("x-refresh-token", tokenStr)
+ return nil
+}
+
+func (h *RedisJWTHandler) ClearToken(ctx *gin.Context) error {
+ ctx.Header("x-jwt-token", "")
+ ctx.Header("x-refresh-token", "")
+
+ claims := ctx.MustGet("users").(*UserClaims)
+ return h.cmd.Set(ctx, fmt.Sprintf("users:ssid:%s", claims.Ssid),
+ "", time.Hour*24*7).Err()
+}
+
+func (h *RedisJWTHandler) CheckSession(ctx *gin.Context, ssid string) error {
+ val, err := h.cmd.Exists(ctx, fmt.Sprintf("users:ssid:%s", ssid)).Result()
+ switch err {
+ case redis.Nil:
+ return nil
+ case nil:
+ if val == 0 {
+ return nil
+ }
+ return errors.New("session 已经无效了")
+ default:
+ return err
+ }
+}
+
+func (h *RedisJWTHandler) ExtractToken(ctx *gin.Context) string {
+ // 我现在用 JWT 来校验
+ tokenHeader := ctx.GetHeader("Authorization")
+ //segs := strings.SplitN(tokenHeader, " ", 2)
+ segs := strings.Split(tokenHeader, " ")
+ if len(segs) != 2 {
+ return ""
+ }
+ return segs[1]
+}
+
+func (h *RedisJWTHandler) SetJWTToken(ctx *gin.Context, uid int64, ssid string) error {
+ claims := UserClaims{
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 30)),
+ },
+ Id: uid,
+ Ssid: ssid,
+ UserAgent: ctx.Request.UserAgent(),
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
+ tokenStr, err := token.SignedString(AtKey)
+ if err != nil {
+ return err
+ }
+ ctx.Header("x-jwt-token", tokenStr)
+ return nil
+}
diff --git a/webook/internal/web/jwt/types.go b/webook/internal/web/jwt/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..375bafb707304bedc127d4c23dcfb6e1de5f837b
--- /dev/null
+++ b/webook/internal/web/jwt/types.go
@@ -0,0 +1,31 @@
+package jwt
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+type Handler interface {
+ SetLoginToken(ctx *gin.Context, uid int64) error
+ SetJWTToken(ctx *gin.Context, uid int64, ssid string) error
+ ClearToken(ctx *gin.Context) error
+ CheckSession(ctx *gin.Context, ssid string) error
+ ExtractToken(ctx *gin.Context) string
+}
+
+type RefreshClaims struct {
+ Uid int64
+ Ssid string
+ jwt.RegisteredClaims
+}
+
+type UserClaims struct {
+ jwt.RegisteredClaims
+ // 声明你自己的要放进去 token 里面的数据
+ Id int64
+ Ssid string
+ // 自己随便加
+ UserAgent string
+
+ VIP bool
+}
diff --git a/webook/internal/web/middleware/login.go b/webook/internal/web/middleware/login.go
new file mode 100644
index 0000000000000000000000000000000000000000..5580c40f0253c4adfb6ef3e3ef9c810cf89b738c
--- /dev/null
+++ b/webook/internal/web/middleware/login.go
@@ -0,0 +1,125 @@
+package middleware
+
+import (
+ "encoding/gob"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "time"
+)
+
+// LoginMiddlewareBuilder 扩展性
+type LoginMiddlewareBuilder struct {
+ paths []string
+}
+
+func NewLoginMiddlewareBuilder() *LoginMiddlewareBuilder {
+ return &LoginMiddlewareBuilder{}
+}
+func (l *LoginMiddlewareBuilder) IgnorePaths(path string) *LoginMiddlewareBuilder {
+ l.paths = append(l.paths, path)
+ return l
+}
+
+func (l *LoginMiddlewareBuilder) Build() gin.HandlerFunc {
+ // 用 Go 的方式编码解码
+ gob.Register(time.Now())
+ return func(ctx *gin.Context) {
+ // 不需要登录校验的
+ for _, path := range l.paths {
+ if ctx.Request.URL.Path == path {
+ return
+ }
+ }
+ // 不需要登录校验的
+ //if ctx.Request.URL.Path == "/users/login" ||
+ // ctx.Request.URL.Path == "/users/signup" {
+ // return
+ //}
+ sess := sessions.Default(ctx)
+ id := sess.Get("userId")
+ if id == nil {
+ // 没有登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ updateTime := sess.Get("update_time")
+ sess.Set("userId", id)
+ sess.Options(sessions.Options{
+ MaxAge: 60,
+ })
+ now := time.Now()
+ // 说明还没有刷新过,刚登陆,还没刷新过
+ if updateTime == nil {
+ sess.Set("update_time", now)
+ if err := sess.Save(); err != nil {
+ panic(err)
+ }
+ }
+ // updateTime 是有的
+ updateTimeVal, _ := updateTime.(time.Time)
+ if now.Sub(updateTimeVal) > time.Second*10 {
+ sess.Set("update_time", now)
+ if err := sess.Save(); err != nil {
+ panic(err)
+ }
+ }
+ }
+}
+
+var IgnorePaths []string
+
+func CheckLogin() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ // 不需要登录校验的
+ for _, path := range IgnorePaths {
+ if ctx.Request.URL.Path == path {
+ return
+ }
+ }
+
+ // 不需要登录校验的
+ //if ctx.Request.URL.Path == "/users/login" ||
+ // ctx.Request.URL.Path == "/users/signup" {
+ // return
+ //}
+ sess := sessions.Default(ctx)
+ id := sess.Get("userId")
+ if id == nil {
+ // 没有登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ }
+}
+
+func CheckLoginV1(paths []string,
+ abc int,
+ bac int64,
+ asdsd string) gin.HandlerFunc {
+ if len(paths) == 0 {
+ paths = []string{}
+ }
+ return func(ctx *gin.Context) {
+ // 不需要登录校验的
+ for _, path := range paths {
+ if ctx.Request.URL.Path == path {
+ return
+ }
+ }
+
+ // 不需要登录校验的
+ //if ctx.Request.URL.Path == "/users/login" ||
+ // ctx.Request.URL.Path == "/users/signup" {
+ // return
+ //}
+ sess := sessions.Default(ctx)
+ id := sess.Get("userId")
+ if id == nil {
+ // 没有登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ }
+}
diff --git a/webook/internal/web/middleware/login_jwt.go b/webook/internal/web/middleware/login_jwt.go
new file mode 100644
index 0000000000000000000000000000000000000000..e88a18957c5d28ac8df13901bf770948f45ffa4e
--- /dev/null
+++ b/webook/internal/web/middleware/login_jwt.go
@@ -0,0 +1,96 @@
+package middleware
+
+import (
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+ "net/http"
+)
+
+// LoginJWTMiddlewareBuilder JWT 登录校验
+type LoginJWTMiddlewareBuilder struct {
+ paths []string
+ ijwt.Handler
+}
+
+func NewLoginJWTMiddlewareBuilder(jwtHdl ijwt.Handler) *LoginJWTMiddlewareBuilder {
+ return &LoginJWTMiddlewareBuilder{
+ Handler: jwtHdl,
+ }
+}
+
+func (l *LoginJWTMiddlewareBuilder) IgnorePaths(path string) *LoginJWTMiddlewareBuilder {
+ l.paths = append(l.paths, path)
+ return l
+}
+
+func (l *LoginJWTMiddlewareBuilder) Build() gin.HandlerFunc {
+ // 用 Go 的方式编码解码
+ return func(ctx *gin.Context) {
+ // 不需要登录校验的
+ for _, path := range l.paths {
+ if ctx.Request.URL.Path == path {
+ return
+ }
+ }
+
+ tokenStr := l.ExtractToken(ctx)
+ claims := ijwt.UserClaims{}
+ // ParseWithClaims 里面,一定要传入指针
+ token, err := jwt.ParseWithClaims(tokenStr, &claims, func(token *jwt.Token) (interface{}, error) {
+ return []byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"), nil
+ })
+ if err != nil {
+ // 没登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ //claims.ExpiresAt.Time.Before(time.Now()) {
+ // // 过期了
+ //}
+ // err 为 nil,token 不为 nil
+ if token == nil || !token.Valid || claims.Id == 0 {
+ // 没登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ if claims.UserAgent != ctx.Request.UserAgent() {
+ // 严重的安全问题
+ // 你是要监控
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ err = l.CheckSession(ctx, claims.Ssid)
+ if err != nil {
+ // 要么 redis 有问题,要么已经退出登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ // 你以为的退出登录,没有用的
+ //token.Valid = false
+ //// tokenStr 是一个新的字符串
+ //tokenStr, err = token.SignedString([]byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"))
+ //if err != nil {
+ // // 记录日志
+ // log.Println("jwt 续约失败", err)
+ //}
+ //ctx.Header("x-jwt-token", tokenStr)
+
+ // 短的 token 过期了,搞个新的
+ //now := time.Now()
+ // 每十秒钟刷新一次
+ //if claims.ExpiresAt.Sub(now) < time.Second*50 {
+ // claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute))
+ // tokenStr, err = token.SignedString([]byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"))
+ // if err != nil {
+ // // 记录日志
+ // log.Println("jwt 续约失败", err)
+ // }
+ // ctx.Header("x-jwt-token", tokenStr)
+ //}
+ ctx.Set("users", claims)
+ //ctx.Set("userId", claims.Id)
+ }
+}
diff --git a/webook/internal/web/middleware/validate_biz.go b/webook/internal/web/middleware/validate_biz.go
new file mode 100644
index 0000000000000000000000000000000000000000..2077468ce3737ca23111245b4ba21ab12dee4dcd
--- /dev/null
+++ b/webook/internal/web/middleware/validate_biz.go
@@ -0,0 +1,14 @@
+package middleware
+
+//func Build() gin.HandlerFunc {
+// return func(ctx *gin.Context) {
+// // order id/order sn
+// bizId := ctx.GetHeader("biz_id")
+// // order
+// biz := ctx.GetHeader("biz")
+// uc := ctx.MustGet("user").(jwt.UserClaims)
+// 单体应用就是数据库,
+// 微服务呢?调用微服务 - 做客户端缓存
+// validate(biz, bizId, uc.Id)
+// }
+//}
diff --git a/webook/internal/web/oberservability.go b/webook/internal/web/oberservability.go
new file mode 100644
index 0000000000000000000000000000000000000000..ce51dfbc95dd8291b32c05492e53ecaab9187985
--- /dev/null
+++ b/webook/internal/web/oberservability.go
@@ -0,0 +1,20 @@
+package web
+
+import (
+ "github.com/gin-gonic/gin"
+ "math/rand"
+ "net/http"
+ "time"
+)
+
+type ObservabilityHandler struct {
+}
+
+func (h *ObservabilityHandler) RegisterRoutes(server *gin.Engine) {
+ g := server.Group("test")
+ g.GET("/metric", func(ctx *gin.Context) {
+ sleep := rand.Int31n(1000)
+ time.Sleep(time.Millisecond * time.Duration(sleep))
+ ctx.String(http.StatusOK, "OK")
+ })
+}
diff --git a/webook/internal/web/result.go b/webook/internal/web/result.go
new file mode 100644
index 0000000000000000000000000000000000000000..d431f630a217a96de4cf5ef8d177948df8dd388f
--- /dev/null
+++ b/webook/internal/web/result.go
@@ -0,0 +1,8 @@
+package web
+
+type Result struct {
+ // 这个叫做业务错误码
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data any `json:"data"`
+}
diff --git a/webook/internal/web/reward.go b/webook/internal/web/reward.go
new file mode 100644
index 0000000000000000000000000000000000000000..990fede89740bb4ee9588de7450bf51c45552631
--- /dev/null
+++ b/webook/internal/web/reward.go
@@ -0,0 +1,71 @@
+package web
+
+import (
+ "context"
+ rewardv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/reward/v1"
+ "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "github.com/gin-gonic/gin"
+ "time"
+)
+
+type RewardHandler struct {
+ client rewardv1.RewardServiceClient
+ //artClient articlev1.ArticleServiceClient
+}
+
+//func NewRewardHandler(client rewardv1.RewardServiceClient,
+// artClient articlev1.ArticleServiceClient) *RewardHandler {
+// return &RewardHandler{client: client, artClient: artClient}
+//}
+
+func (h *RewardHandler) RegisterRoutes(server *gin.Engine) {
+ rg := server.Group("/reward")
+ rg.POST("/detail",
+ ginx.WrapBodyAndToken[GetRewardReq](h.GetReward))
+ //rg.POST("/article",
+ // ginx.WrapBodyAndToken[GetRewardReq](h.GetReward))
+}
+
+type GetRewardReq struct {
+ Rid int64
+}
+
+// GetReward 前端传过来一个超长的超时时间,例如说 10s
+// 后端去轮询
+// 可能引来巨大的性能问题
+// 真正优雅的还是前端来轮询
+// stream
+func (h *RewardHandler) GetReward(
+ ctx *gin.Context,
+ req GetRewardReq,
+ claims jwt.UserClaims) (ginx.Result, error) {
+
+ for {
+ newCtx, cancel := context.WithTimeout(ctx, time.Second)
+ resp, err := h.client.GetReward(newCtx, &rewardv1.GetRewardRequest{
+ Rid: req.Rid,
+ Uid: claims.Id,
+ })
+ cancel()
+ if err != nil {
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统错误",
+ }, err
+ }
+ if resp.Status == 1 {
+ continue
+ }
+ return ginx.Result{
+ // 暂时也就是只需要状态
+ Data: resp.Status.String(),
+ }, nil
+ }
+
+}
+
+type RewardArticleReq struct {
+ Aid int64 `json:"aid"`
+ Amt int64 `json:"amt"`
+}
diff --git a/webook/internal/web/sqlx_store/store.go b/webook/internal/web/sqlx_store/store.go
new file mode 100644
index 0000000000000000000000000000000000000000..dac629b0e1dc6e31393a0c2a1091acd47f922d91
--- /dev/null
+++ b/webook/internal/web/sqlx_store/store.go
@@ -0,0 +1,30 @@
+package sqlx_store
+
+import (
+ ginSession "github.com/gin-contrib/sessions"
+ "github.com/gorilla/sessions"
+ "net/http"
+)
+
+type Store struct {
+}
+
+func (s *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (s *Store) New(r *http.Request, name string) (*sessions.Session, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (st *Store) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (s *Store) Options(options ginSession.Options) {
+ //TODO implement me
+ panic("implement me")
+}
diff --git a/webook/internal/web/types.go b/webook/internal/web/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..b74f29c5eb802740e181e50f3af619d1652b2d52
--- /dev/null
+++ b/webook/internal/web/types.go
@@ -0,0 +1,7 @@
+package web
+
+import "github.com/gin-gonic/gin"
+
+type handler interface {
+ RegisterRoutes(server *gin.Engine)
+}
diff --git a/webook/internal/web/user.go b/webook/internal/web/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6468c65d91011cff92c7ae34a0aa07a2919cf51
--- /dev/null
+++ b/webook/internal/web/user.go
@@ -0,0 +1,402 @@
+package web
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/errs"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ regexp "github.com/dlclark/regexp2"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/redis/go-redis/v9"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+ "net/http"
+)
+
+const biz = "login"
+
+// 确保 UserHandler 上实现了 handler 接口
+var _ handler = &UserHandler{}
+
+// 这个更优雅
+var _ handler = (*UserHandler)(nil)
+
+// UserHandler 我准备在它上面定义跟用户有关的路由
+type UserHandler struct {
+ svc service.UserService
+ codeSvc service.CodeService
+ emailExp *regexp.Regexp
+ passwordExp *regexp.Regexp
+ ijwt.Handler
+ cmd redis.Cmdable
+}
+
+func NewUserHandler(svc service.UserService,
+ codeSvc service.CodeService, jwtHdl ijwt.Handler) *UserHandler {
+ const (
+ emailRegexPattern = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
+ passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
+ )
+ emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
+ passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
+ return &UserHandler{
+ svc: svc,
+ emailExp: emailExp,
+ passwordExp: passwordExp,
+ codeSvc: codeSvc,
+ Handler: jwtHdl,
+ }
+}
+
+func (u *UserHandler) RegisterRoutesV1(ug *gin.RouterGroup) {
+ ug.GET("/profile", u.Profile)
+ ug.POST("/signup", u.SignUp)
+ ug.POST("/login", u.Login)
+ ug.POST("/edit", u.Edit)
+}
+
+func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
+ ug := server.Group("/users")
+ ug.GET("/profile", u.ProfileJWT)
+ ug.POST("/signup", u.SignUp)
+ //ug.POST("/login", u.Login)
+ ug.POST("/login", u.LoginJWT)
+ ug.POST("/logout", u.LogoutJWT)
+ ug.POST("/edit", u.Edit)
+ // PUT "/login/sms/code" 发验证码
+ // POST "/login/sms/code" 校验验证码
+ // POST /sms/login/code
+ // POST /code/sms
+ ug.POST("/login_sms/code/send", u.SendLoginSMSCode)
+ ug.POST("/login_sms", u.LoginSMS)
+ ug.POST("/refresh_token", u.RefreshToken)
+}
+
+func (u *UserHandler) LogoutJWT(ctx *gin.Context) {
+ err := u.ClearToken(ctx)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "退出登录失败",
+ })
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "退出登录OK",
+ })
+}
+
+// RefreshToken 可以同时刷新长短 token,用 redis 来记录是否有效,即 refresh_token 是一次性的
+// 参考登录校验部分,比较 User-Agent 来增强安全性
+func (u *UserHandler) RefreshToken(ctx *gin.Context) {
+ ctx.Request.Context()
+ // 只有这个接口,拿出来的才是 refresh_token,其它地方都是 access token
+ refreshToken := u.ExtractToken(ctx)
+ var rc ijwt.RefreshClaims
+ token, err := jwt.ParseWithClaims(refreshToken, &rc, func(token *jwt.Token) (interface{}, error) {
+ return ijwt.RtKey, nil
+ })
+ if err != nil || !token.Valid {
+ zap.L().Error("系统异常", zap.Error(err))
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ err = u.CheckSession(ctx, rc.Ssid)
+ if err != nil {
+ // 信息量不足
+ zap.L().Error("系统异常", zap.Error(err))
+ // 要么 redis 有问题,要么已经退出登录
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ // 搞个新的 access_token
+ err = u.SetJWTToken(ctx, rc.Uid, rc.Ssid)
+ if err != nil {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ zap.L().Error("系统异常", zap.Error(err))
+ // 正常来说,msg 的部分就应该包含足够的定位信息
+ zap.L().Error("ijoihpidf 设置 JWT token 出现异常",
+ zap.Error(err),
+ zap.String("method", "UserHandler:RefreshToken"))
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "刷新成功",
+ })
+}
+
+func (u *UserHandler) LoginSMS(ctx *gin.Context) {
+ type Req struct {
+ Phone string `json:"phone"`
+ Code string `json:"code"`
+ }
+ var req Req
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+
+ // 这边,可以加上各种校验
+ ok, err := u.codeSvc.Verify(ctx, biz, req.Phone, req.Code)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ zap.L().Error("校验验证码出错", zap.Error(err),
+ // 不能这样打,因为手机号码是敏感数据,你不能达到日志里面
+ // 打印加密后的串
+ // 脱敏,152****1234
+ zap.String("手机号码", req.Phone))
+ // 最多最多就这样
+ zap.L().Debug("", zap.String("手机号码", req.Phone))
+ return
+ }
+ if !ok {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 4,
+ Msg: "验证码有误",
+ })
+ return
+ }
+
+ // 我这个手机号,会不会是一个新用户呢?
+ // 这样子
+ user, err := u.svc.FindOrCreate(ctx, req.Phone)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+
+ if err = u.SetLoginToken(ctx, user.Id); err != nil {
+ // 记录日志
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "验证码校验通过",
+ })
+}
+
+func (u *UserHandler) SendLoginSMSCode(ctx *gin.Context) {
+ type Req struct {
+ Phone string `json:"phone"`
+ }
+ var req Req
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ // 是不是一个合法的手机号码
+ // 考虑正则表达式
+ if req.Phone == "" {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 4,
+ Msg: "输入有误",
+ })
+ return
+ }
+ err := u.codeSvc.Send(ctx, biz, req.Phone)
+ switch err {
+ case nil:
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "发送成功",
+ })
+ case service.ErrCodeSendTooMany:
+ zap.L().Warn("短信发送太频繁",
+ zap.Error(err))
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "发送太频繁,请稍后再试",
+ })
+ default:
+ zap.L().Error("短信发送失败",
+ zap.Error(err))
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ }
+}
+
+func (u *UserHandler) SignUp(ctx *gin.Context) {
+ type SignUpReq struct {
+ Email string `json:"email"`
+ ConfirmPassword string `json:"confirmPassword"`
+ Password string `json:"password"`
+ }
+
+ var req SignUpReq
+ // Bind 方法会根据 Content-Type 来解析你的数据到 req 里面
+ // 解析错了,就会直接写回一个 400 的错误
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+
+ ok, err := u.emailExp.MatchString(req.Email)
+ if err != nil {
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+ if !ok {
+ ctx.String(http.StatusOK, "你的邮箱格式不对")
+ return
+ }
+ if req.ConfirmPassword != req.Password {
+ ctx.String(http.StatusOK, "两次输入的密码不一致")
+ return
+ }
+ ok, err = u.passwordExp.MatchString(req.Password)
+ if err != nil {
+ // 记录日志
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+ if !ok {
+ ctx.String(http.StatusOK, "密码必须大于8位,包含数字、特殊字符")
+ return
+ }
+
+ // 调用一下 svc 的方法
+ err = u.svc.SignUp(ctx.Request.Context(), domain.User{
+ Email: req.Email,
+ Password: req.Password,
+ })
+ if err == service.ErrUserDuplicateEmail {
+ // 这是复用
+ span := trace.SpanFromContext(ctx.Request.Context())
+ span.AddEvent("邮件冲突")
+ ctx.String(http.StatusOK, "邮箱冲突")
+ return
+ }
+ if err != nil {
+ ctx.String(http.StatusOK, "系统异常")
+ return
+ }
+
+ ctx.String(http.StatusOK, "注册成功")
+}
+
+func (u *UserHandler) LoginJWT(ctx *gin.Context) {
+ type LoginReq struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+ }
+
+ var req LoginReq
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ user, err := u.svc.Login(ctx, req.Email, req.Password)
+ if err == service.ErrInvalidUserOrPassword {
+ ctx.String(http.StatusOK, "用户名或密码不对")
+ return
+ }
+ if err != nil {
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+
+ if err = u.SetLoginToken(ctx, user.Id); err != nil {
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+
+ ctx.String(http.StatusOK, "登录成功")
+ return
+}
+
+func (u *UserHandler) Login(ctx *gin.Context) {
+ type LoginReq struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+ }
+
+ var req LoginReq
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+ user, err := u.svc.Login(ctx, req.Email, req.Password)
+ if err == service.ErrInvalidUserOrPassword {
+ ctx.JSON(http.StatusOK, Result{
+ Code: errs.UserInvalidOrPassword,
+ Msg: "用户不存在或者密码错误",
+ })
+ return
+ }
+ if err != nil {
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+
+ // 步骤2
+ // 在这里登录成功了
+ // 设置 session
+ sess := sessions.Default(ctx)
+ // 我可以随便设置值了
+ // 你要放在 session 里面的值
+ sess.Set("userId", user.Id)
+ sess.Options(sessions.Options{
+ Secure: true,
+ HttpOnly: true,
+ // 一分钟过期
+ MaxAge: 60,
+ })
+ sess.Save()
+ ctx.String(http.StatusOK, "登录成功")
+ return
+}
+
+//func (u *UserHandler) Do(fn func(ctx *gin.Context) (any, error)) {
+// data, err := fn(ctx)
+// if err != nil {
+// 在这里打日志
+// }
+//}
+
+func (u *UserHandler) Logout(ctx *gin.Context) {
+ sess := sessions.Default(ctx)
+ // 我可以随便设置值了
+ // 你要放在 session 里面的值
+ sess.Options(sessions.Options{
+ //Secure: true,
+ //HttpOnly: true,
+ MaxAge: -1,
+ })
+ sess.Save()
+ ctx.String(http.StatusOK, "退出登录成功")
+}
+
+func (u *UserHandler) Edit(ctx *gin.Context) {
+
+}
+
+func (u *UserHandler) ProfileJWT(ctx *gin.Context) {
+ c, _ := ctx.Get("users")
+ // 你可以断定,必然有 claims
+ //if !ok {
+ // // 你可以考虑监控住这里
+ // ctx.String(http.StatusOK, "系统错误")
+ // return
+ //}
+ // ok 代表是不是 *UserClaims
+ claims, ok := c.(ijwt.UserClaims)
+ if !ok {
+ // 你可以考虑监控住这里
+ ctx.String(http.StatusOK, "系统错误")
+ return
+ }
+ println(claims.Id)
+ ctx.String(http.StatusOK, "你的 profile")
+ // 这边就是你补充 profile 的其它代码
+}
+
+func (u *UserHandler) Profile(ctx *gin.Context) {
+ ctx.String(http.StatusOK, "这是你的 Profile")
+}
diff --git a/webook/internal/web/user_test.go b/webook/internal/web/user_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a50c05f17bab45122fd013c256f0429db123d5a5
--- /dev/null
+++ b/webook/internal/web/user_test.go
@@ -0,0 +1,231 @@
+package web
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ svcmocks "gitee.com/geekbang/basic-go/webook/internal/service/mocks"
+ "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+ "golang.org/x/crypto/bcrypt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestEncrypt(t *testing.T) {
+ _ = NewUserHandler(nil, nil, nil)
+ password := "hello#world123"
+ encrypted, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = bcrypt.CompareHashAndPassword(encrypted, []byte(password))
+ assert.NoError(t, err)
+}
+
+func TestNil(t *testing.T) {
+ testTypeAssert(nil)
+}
+
+func testTypeAssert(c any) {
+ _, ok := c.(jwt.UserClaims)
+ println(ok)
+}
+
+func TestUserHandler_SignUp(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ mock func(ctrl *gomock.Controller) service.UserService
+
+ reqBody string
+
+ wantCode int
+ wantBody string
+ }{
+ {
+ name: "注册成功",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
+ Email: "123@qq.com",
+ Password: "hello#world123",
+ }).Return(nil)
+ // 注册成功是 return nil
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello#world123",
+ "confirmPassword": "hello#world123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "注册成功",
+ },
+ {
+ name: "参数不对,bind 失败",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ // 注册成功是 return nil
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello#world123"
+`,
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ name: "邮箱格式不对",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@q",
+ "password": "hello#world123",
+ "confirmPassword": "hello#world123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "你的邮箱格式不对",
+ },
+ {
+ name: "两次输入密码不匹配",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello#world1234",
+ "confirmPassword": "hello#world123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "两次输入的密码不一致",
+ },
+ {
+ name: "密码格式不对",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ return usersvc
+ },
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello123",
+ "confirmPassword": "hello123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "密码必须大于8位,包含数字、特殊字符",
+ },
+ {
+ name: "邮箱冲突",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
+ Email: "123@qq.com",
+ Password: "hello#world123",
+ }).Return(service.ErrUserDuplicateEmail)
+ // 注册成功是 return nil
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello#world123",
+ "confirmPassword": "hello#world123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "邮箱冲突",
+ },
+ {
+ name: "系统异常",
+ mock: func(ctrl *gomock.Controller) service.UserService {
+ usersvc := svcmocks.NewMockUserService(ctrl)
+ usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
+ Email: "123@qq.com",
+ Password: "hello#world123",
+ }).Return(errors.New("随便一个 error"))
+ // 注册成功是 return nil
+ return usersvc
+ },
+
+ reqBody: `
+{
+ "email": "123@qq.com",
+ "password": "hello#world123",
+ "confirmPassword": "hello#world123"
+}
+`,
+ wantCode: http.StatusOK,
+ wantBody: "系统异常",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ server := gin.Default()
+ // 用不上 codeSvc
+ h := NewUserHandler(tc.mock(ctrl), nil, nil)
+ h.RegisterRoutes(server)
+
+ req, err := http.NewRequest(http.MethodPost,
+ "/users/signup", bytes.NewBuffer([]byte(tc.reqBody)))
+ require.NoError(t, err)
+ // 数据是 JSON 格式
+ req.Header.Set("Content-Type", "application/json")
+ // 这里你就可以继续使用 req
+
+ resp := httptest.NewRecorder()
+ // 这就是 HTTP 请求进去 GIN 框架的入口。
+ // 当你这样调用的时候,GIN 就会处理这个请求
+ // 响应写回到 resp 里
+ server.ServeHTTP(resp, req)
+
+ assert.Equal(t, tc.wantCode, resp.Code)
+ assert.Equal(t, tc.wantBody, resp.Body.String())
+
+ })
+ }
+}
+
+func TestMock(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ usersvc := svcmocks.NewMockUserService(ctrl)
+
+ usersvc.EXPECT().SignUp(gomock.Any(), gomock.Any()).
+ Return(errors.New("mock error"))
+
+ //usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
+ // Email: "124@qq.com",
+ //}).Return(errors.New("mock error"))
+
+ err := usersvc.SignUp(context.Background(), domain.User{
+ Email: "123@qq.com",
+ })
+ t.Log(err)
+}
diff --git a/webook/internal/web/wechat.go b/webook/internal/web/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..5dc5f1671dd09a2e32514860def8482d0c5008c4
--- /dev/null
+++ b/webook/internal/web/wechat.go
@@ -0,0 +1,188 @@
+package web
+
+import (
+ "errors"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/internal/service/oauth2/wechat"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+ uuid "github.com/lithammer/shortuuid/v4"
+ "net/http"
+ "time"
+)
+
+type OAuth2WechatHandler struct {
+ svc wechat.Service
+ userSvc service.UserService
+ ijwt.Handler
+ stateKey []byte
+ //cfg WechatHandlerConfig
+}
+
+//type WechatHandlerConfig struct {
+// Secure bool
+// //StateKey
+//}
+
+func NewOAuth2WechatHandler(svc wechat.Service,
+ userSvc service.UserService,
+ jwtHdl ijwt.Handler) *OAuth2WechatHandler {
+ return &OAuth2WechatHandler{
+ svc: svc,
+ userSvc: userSvc,
+ Handler: jwtHdl,
+ stateKey: []byte("95osj3fUD7foxmlYdDbncXz4VD2igvf1"),
+ //cfg: cfg,
+ }
+}
+
+func (h *OAuth2WechatHandler) RegisterRoutes(server *gin.Engine) {
+ g := server.Group("/oauth2/wechat")
+ g.GET("/authurl", h.AuthURL)
+ g.Any("/callback", h.Callback)
+}
+
+func (h *OAuth2WechatHandler) AuthURL(ctx *gin.Context) {
+ state := uuid.New()
+ url, err := h.svc.AuthURL(ctx, state)
+ // 要把我的 state 存好
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "构造扫码登录URL失败",
+ })
+ return
+ }
+ if err = h.setStateCookie(ctx, state); err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统异常",
+ })
+ return
+ }
+ ctx.JSON(http.StatusOK, Result{
+ Data: url,
+ })
+}
+
+func (h *OAuth2WechatHandler) setStateCookie(ctx *gin.Context, state string) error {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, StateClaims{
+ State: state,
+ RegisteredClaims: jwt.RegisteredClaims{
+ // 过期时间,你预期中一个用户完成登录的时间
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 10)),
+ },
+ })
+ tokenStr, err := token.SignedString(h.stateKey)
+ if err != nil {
+ return err
+ }
+ ctx.SetCookie("jwt-state", tokenStr,
+ 600, "/oauth2/wechat/callback",
+ // 线上把 secure 做成 true
+ "", false, true)
+ return nil
+}
+
+func (h *OAuth2WechatHandler) Callback(ctx *gin.Context) {
+ code := ctx.Query("code")
+ err := h.verifyState(ctx)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "登录失败",
+ })
+ return
+ }
+ info, err := h.svc.VerifyCode(ctx, code)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+ // 这里怎么办?
+ // 从 userService 里面拿 uid
+ u, err := h.userSvc.FindOrCreateByWechat(ctx, info)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+
+ err = h.SetLoginToken(ctx, u.Id)
+ if err != nil {
+ ctx.JSON(http.StatusOK, Result{
+ Code: 5,
+ Msg: "系统错误",
+ })
+ return
+ }
+
+ ctx.JSON(http.StatusOK, Result{
+ Msg: "OK",
+ })
+ // 验证微信的 code
+}
+
+func (h *OAuth2WechatHandler) verifyState(ctx *gin.Context) error {
+ state := ctx.Query("state")
+ // 校验一下我的 state
+ ck, err := ctx.Cookie("jwt-state")
+ if err != nil {
+ return fmt.Errorf("拿不到 state 的 cookie, %w", err)
+ }
+
+ var sc StateClaims
+ token, err := jwt.ParseWithClaims(ck, &sc, func(token *jwt.Token) (interface{}, error) {
+ return h.stateKey, nil
+ })
+ if err != nil || !token.Valid {
+ return fmt.Errorf("token 已经过期了, %w", err)
+ }
+
+ if sc.State != state {
+ return errors.New("state 不相等")
+ }
+ return nil
+}
+
+type StateClaims struct {
+ State string
+ jwt.RegisteredClaims
+}
+
+//type OAuth2Handler struct {
+// wechatService
+// dingdingService
+// feishuService
+//
+// svcs map[string]OAuth2Service
+//}
+
+//func (h *OAuth2Handler) RegisterRoutes(server *gin.Engine) {
+// // 统一处理所有的 OAuth2 的
+// g := server.Group("/oauth2")
+// g.GET("/:platform/authurl", h.AuthURL)
+// g.Any("/:platform/callback", h.Callback)
+//}
+
+//func (h *OAuth2Handler) AuthURL(ctx *gin.Context) {
+// platform := ctx.Param("platform")
+// switch platform {
+// case "wechat":
+// h.wechatService.AuthURL
+// }
+//
+// svc := h.svcs[platform]
+// svc.
+//}
+
+//func (h *OAuth2Handler) Callback(ctx *gin.Context) {
+//
+//}
diff --git a/webook/ioc/config.go b/webook/ioc/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..608388b8135bdf697793df649775c3b0e9254e9e
--- /dev/null
+++ b/webook/ioc/config.go
@@ -0,0 +1,42 @@
+package ioc
+
+import (
+ "context"
+ "github.com/spf13/viper"
+)
+
+type Configer interface {
+ GetString(ctx context.Context, key string) (string, error)
+ MustGetString(ctx context.Context, key string) string
+ GetStringOrDefault(ctc context.Context, key string, def string) string
+
+ //Unmarshal()
+}
+
+type ViperConfigerAdapter struct {
+ v *viper.Viper
+}
+
+type myConfiger struct {
+}
+
+func (m *myConfiger) GetString(ctx context.Context, key string) (string, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (m *myConfiger) MustGetString(ctx context.Context, key string) string {
+ str, err := m.GetString(ctx, key)
+ if err != nil {
+ panic(err)
+ }
+ return str
+}
+
+func (m *myConfiger) GetStringOrDefault(ctx context.Context, key string, def string) string {
+ str, err := m.GetString(ctx, key)
+ if err != nil {
+ return def
+ }
+ return str
+}
diff --git a/webook/ioc/db.go b/webook/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..322c17caa9619f1c7979a5615a33fc686a200a30
--- /dev/null
+++ b/webook/ioc/db.go
@@ -0,0 +1,257 @@
+package ioc
+
+import (
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ promsdk "github.com/prometheus/client_golang/prometheus"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ glogger "gorm.io/gorm/logger"
+ "gorm.io/plugin/opentelemetry/tracing"
+ "gorm.io/plugin/prometheus"
+ "time"
+)
+
+func InitDB(l logger.LoggerV1) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+
+ // 有些人的做法
+ // localhost:13316
+ //Addr string
+ //// localhost
+ //Domain string
+ //// 13316
+ //Port string
+ //Protocol string
+ //// root
+ //Username string
+ //// root
+ //Password string
+ //// webook
+ //DBName string
+ }
+ var cfg = Config{
+ DSN: "root:root@tcp(localhost:13316)/webook_default",
+ }
+ // 看起来,remote 不支持 key 的切割
+ err := viper.UnmarshalKey("db", &cfg)
+ //dsn := viper.GetString("db.mysql")
+ //println(dsn)
+ //if err != nil {
+ // panic(err)
+ //}
+ db, err := gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{
+ // 缺了一个 writer
+ Logger: glogger.New(gormLoggerFunc(l.Debug), glogger.Config{
+ // 慢查询阈值,只有执行时间超过这个阈值,才会使用
+ // 50ms, 100ms
+ // SQL 查询必然要求命中索引,最好就是走一次磁盘 IO
+ // 一次磁盘 IO 是不到 10ms
+ SlowThreshold: time.Millisecond * 10,
+ IgnoreRecordNotFoundError: true,
+ ParameterizedQueries: true,
+ LogLevel: glogger.Info,
+ }),
+ })
+ if err != nil {
+ // 我只会在初始化过程中 panic
+ // panic 相当于整个 goroutine 结束
+ // 一旦初始化过程出错,应用就不要启动了
+ panic(err)
+ }
+
+ err = db.Use(prometheus.New(prometheus.Config{
+ DBName: "webook",
+ RefreshInterval: 15,
+ StartServer: false,
+ MetricsCollector: []prometheus.MetricsCollector{
+ &prometheus.MySQL{
+ VariableNames: []string{"thread_running"},
+ },
+ },
+ }))
+ if err != nil {
+ panic(err)
+ }
+
+ // 监控查询的执行时间
+ pcb := newCallbacks()
+ //pcb.registerAll(db)
+ db.Use(pcb)
+
+ db.Use(tracing.NewPlugin(tracing.WithDBName("webook"),
+ tracing.WithQueryFormatter(func(query string) string {
+ l.Debug("", logger.String("query", query))
+ return query
+
+ }),
+ // 不要记录 metrics
+ tracing.WithoutMetrics(),
+ // 不要记录查询参数
+ tracing.WithoutQueryVariables()))
+
+ //dao.NewUserDAOV1(func() *gorm.DB {
+ //viper.OnConfigChange(func(in fsnotify.Event) {
+ //oldDB := db
+ //db, err = gorm.Open(mysql.Open())
+ //pt := unsafe.Pointer(&db)
+ //atomic.StorePointer(&pt, unsafe.Pointer(&db))
+ //oldDB.Close()
+ //})
+ // 要用原子操作
+ //return db
+ //})
+
+ err = dao.InitTable(db)
+ if err != nil {
+ panic(err)
+ }
+
+ err = dao2.InitTable(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type Callbacks struct {
+ vector *promsdk.SummaryVec
+}
+
+func (pcb *Callbacks) Name() string {
+ return "prometheus-query"
+}
+
+func (pcb *Callbacks) Initialize(db *gorm.DB) error {
+ pcb.registerAll(db)
+ return nil
+}
+
+func newCallbacks() *Callbacks {
+ vector := promsdk.NewSummaryVec(promsdk.SummaryOpts{
+ // 在这边,你要考虑设置各种 Namespace
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Name: "gorm_query_time",
+ Help: "统计 GORM 的执行时间",
+ ConstLabels: map[string]string{
+ "db": "webook",
+ },
+ Objectives: map[float64]float64{
+ 0.5: 0.01,
+ 0.9: 0.01,
+ 0.99: 0.005,
+ 0.999: 0.0001,
+ },
+ },
+ // 如果是 JOIN 查询,table 就是 JOIN 在一起的
+ // 或者 table 就是主表,A JOIN B,记录的是 A
+ []string{"type", "table"})
+
+ pcb := &Callbacks{
+ vector: vector,
+ }
+ promsdk.MustRegister(vector)
+ return pcb
+}
+
+func (pcb *Callbacks) registerAll(db *gorm.DB) {
+ // 作用于 INSERT 语句
+ err := db.Callback().Create().Before("*").
+ Register("prometheus_create_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Create().After("*").
+ Register("prometheus_create_after", pcb.after("create"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Update().Before("*").
+ Register("prometheus_update_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Update().After("*").
+ Register("prometheus_update_after", pcb.after("update"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Delete().Before("*").
+ Register("prometheus_delete_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Delete().After("*").
+ Register("prometheus_delete_after", pcb.after("delete"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Raw().Before("*").
+ Register("prometheus_raw_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Raw().After("*").
+ Register("prometheus_raw_after", pcb.after("raw"))
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Callback().Row().Before("*").
+ Register("prometheus_row_before", pcb.before())
+ if err != nil {
+ panic(err)
+ }
+ err = db.Callback().Row().After("*").
+ Register("prometheus_row_after", pcb.after("row"))
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (c *Callbacks) before() func(db *gorm.DB) {
+ return func(db *gorm.DB) {
+ startTime := time.Now()
+ db.Set("start_time", startTime)
+ }
+}
+
+func (c *Callbacks) after(typ string) func(db *gorm.DB) {
+ return func(db *gorm.DB) {
+ val, _ := db.Get("start_time")
+ startTime, ok := val.(time.Time)
+ if !ok {
+ // 你啥都干不了
+ return
+ }
+ table := db.Statement.Table
+ if table == "" {
+ table = "unknown"
+ }
+ c.vector.WithLabelValues(typ, table).
+ Observe(float64(time.Since(startTime).Milliseconds()))
+ }
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
+
+type DoSomething interface {
+ DoABC() string
+}
+
+type DoSomethingFunc func() string
+
+func (d DoSomethingFunc) DoABC() string {
+ return d()
+}
diff --git a/webook/ioc/doc.go b/webook/ioc/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..5cfb3c1b89ba1abfdb88592112c755288ad5822f
--- /dev/null
+++ b/webook/ioc/doc.go
@@ -0,0 +1,2 @@
+// Package ioc 依赖反转
+package ioc
diff --git a/webook/ioc/intr.go b/webook/ioc/intr.go
new file mode 100644
index 0000000000000000000000000000000000000000..8947f23a0184f9c3ab10f1408535734e1c25fd82
--- /dev/null
+++ b/webook/ioc/intr.go
@@ -0,0 +1,96 @@
+package ioc
+
+import (
+ intrv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/intr/v1"
+ "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "gitee.com/geekbang/basic-go/webook/internal/web/client"
+ "github.com/fsnotify/fsnotify"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "go.etcd.io/etcd/client/v3/naming/resolver"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+func InitEtcd() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ cli, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return cli
+}
+
+// 真正的 gRPC 的客户端
+func InitIntrGRPCClientV1(client *clientv3.Client) intrv1.InteractiveServiceClient {
+ type Config struct {
+ Secure bool
+ Name string
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.client.intr", &cfg)
+ if err != nil {
+ panic(err)
+ }
+
+ bd, err := resolver.NewBuilder(client)
+ if err != nil {
+ panic(err)
+ }
+
+ opts := []grpc.DialOption{grpc.WithResolvers(bd)}
+ if cfg.Secure {
+ // 上面,要去加载你的证书之类的东西
+ // 启用 HTTPS
+ } else {
+ opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ }
+ // 这个地方你没填对,它也不会报错
+ cc, err := grpc.Dial("etcd:///service/"+cfg.Name, opts...)
+ if err != nil {
+ panic(err)
+ }
+ return intrv1.NewInteractiveServiceClient(cc)
+}
+
+// InitIntrGRPCClient 这个是我们流量控制的客户端
+func InitIntrGRPCClient(svc service.InteractiveService) intrv1.InteractiveServiceClient {
+ type Config struct {
+ Addr string
+ Secure bool
+ Threshold int32
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.client.intr", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ var opts []grpc.DialOption
+ if cfg.Secure {
+ // 上面,要去加载你的证书之类的东西
+ // 启用 HTTPS
+ } else {
+ opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ }
+ cc, err := grpc.Dial(cfg.Addr, opts...)
+ if err != nil {
+ panic(err)
+ }
+ remote := intrv1.NewInteractiveServiceClient(cc)
+ local := client.NewInteractiveServiceAdapter(svc)
+ res := client.NewGreyScaleInteractiveServiceClient(remote, local)
+ // 我的习惯是在这里监听
+ viper.OnConfigChange(func(in fsnotify.Event) {
+ var cfg Config
+ err = viper.UnmarshalKey("grpc.client.intr", &cfg)
+ if err != nil {
+ // 你可以输出日志
+ }
+ res.UpdateThreshold(cfg.Threshold)
+ })
+ return res
+}
diff --git a/webook/ioc/kafka.go b/webook/ioc/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..5e8dd3310cce20e641a398b23708dcb6c118d1d5
--- /dev/null
+++ b/webook/ioc/kafka.go
@@ -0,0 +1,38 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/events"
+ "github.com/IBM/sarama"
+ "github.com/spf13/viper"
+)
+
+func InitKafka() sarama.Client {
+ type Config struct {
+ Addrs []string `yaml:"addrs"`
+ }
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ var cfg Config
+ err := viper.UnmarshalKey("kafka", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := sarama.NewClient(cfg.Addrs, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+func NewSyncProducer(client sarama.Client) sarama.SyncProducer {
+ res, err := sarama.NewSyncProducerFromClient(client)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
+
+// NewConsumers 面临的问题依旧是所有的 Consumer 在这里注册一下
+func NewConsumers() []events.Consumer {
+ return []events.Consumer{}
+}
diff --git a/webook/ioc/log.go b/webook/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2f96c0463bffaf4f05a8de79fc885006a88c232
--- /dev/null
+++ b/webook/ioc/log.go
@@ -0,0 +1,14 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ l, err := zap.NewDevelopment()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/ioc/mysql_job.go b/webook/ioc/mysql_job.go
new file mode 100644
index 0000000000000000000000000000000000000000..85f3fd5ae379700ec5d0772fa34ae6c7d2990f55
--- /dev/null
+++ b/webook/ioc/mysql_job.go
@@ -0,0 +1,30 @@
+package ioc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/internal/domain"
+ "gitee.com/geekbang/basic-go/webook/internal/job"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "time"
+)
+
+func InitScheduler(l logger.LoggerV1,
+ local *job.LocalFuncExecutor,
+ svc service.JobService) *job.Scheduler {
+ res := job.NewScheduler(svc, l)
+ res.RegisterExecutor(local)
+ return res
+}
+
+func InitLocalFuncExecutor(svc service.RankingService) *job.LocalFuncExecutor {
+ res := job.NewLocalFuncExecutor()
+ // 要在数据库里面插入一条记录。
+ // ranking job 的记录,通过管理任务接口来插入
+ res.RegisterFunc("ranking", func(ctx context.Context, j domain.Job) error {
+ ctx, cancel := context.WithTimeout(ctx, time.Second*30)
+ defer cancel()
+ return svc.TopN(ctx)
+ })
+ return res
+}
diff --git a/webook/ioc/otel.go b/webook/ioc/otel.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8e52343523c1906f07ce0132cb0b9a1fa61e652
--- /dev/null
+++ b/webook/ioc/otel.go
@@ -0,0 +1,104 @@
+package ioc
+
+import (
+ "context"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/exporters/zipkin"
+ "go.opentelemetry.io/otel/propagation"
+ "go.opentelemetry.io/otel/sdk/resource"
+ "go.opentelemetry.io/otel/sdk/trace"
+ semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
+ trace2 "go.opentelemetry.io/otel/trace"
+ "time"
+)
+
+func InitOTEL() func(ctx context.Context) {
+ res, err := newResource("webook", "v0.0.1")
+ if err != nil {
+ panic(err)
+ }
+ prop := newPropagator()
+ otel.SetTextMapPropagator(prop)
+
+ tp, err := newTraceProvider(res)
+ if err != nil {
+ panic(err)
+ }
+ otel.SetTracerProvider(tp)
+ newTp := &MyTracerProvider{
+ Enable: true,
+ nopProvider: trace2.NewNoopTracerProvider(),
+ provider: tp,
+ }
+ // 监听配置变更就可以了
+ otel.SetTracerProvider(newTp)
+
+ return func(ctx context.Context) {
+ tp.Shutdown(ctx)
+ }
+}
+
+func newResource(serviceName, serviceVersion string) (*resource.Resource, error) {
+ return resource.Merge(resource.Default(),
+ resource.NewWithAttributes(semconv.SchemaURL,
+ semconv.ServiceName(serviceName),
+ semconv.ServiceVersion(serviceVersion),
+ ))
+}
+
+func newTraceProvider(res *resource.Resource) (*trace.TracerProvider, error) {
+ exporter, err := zipkin.New(
+ "http://localhost:9411/api/v2/spans")
+ if err != nil {
+ return nil, err
+ }
+
+ traceProvider := trace.NewTracerProvider(
+ trace.WithBatcher(exporter,
+ // Default is 5s. Set to 1s for demonstrative purposes.
+ trace.WithBatchTimeout(time.Second)),
+ trace.WithResource(res),
+ )
+ return traceProvider, nil
+}
+
+func newPropagator() propagation.TextMapPropagator {
+ return propagation.NewCompositeTextMapPropagator(
+ propagation.TraceContext{},
+ propagation.Baggage{},
+ )
+}
+
+type MyTracerProvider struct {
+ // 改原子操作
+ Enable bool
+ nopProvider trace2.TracerProvider
+ provider trace2.TracerProvider
+}
+
+func (m *MyTracerProvider) Tracer(name string, options ...trace2.TracerOption) trace2.Tracer {
+ if m.Enable {
+ return m.provider.Tracer(name, options...)
+ }
+ return m.nopProvider.Tracer(name, options...)
+}
+
+func (m *MyTracerProvider) TracerV1(name string, options ...trace2.TracerOption) trace2.Tracer {
+ return &MyTracer{
+ nopTracer: m.nopProvider.Tracer(name, options...),
+ tracer: m.provider.Tracer(name, options...),
+ }
+}
+
+type MyTracer struct {
+ Enable bool
+ nopTracer trace2.Tracer
+ tracer trace2.Tracer
+}
+
+func (m *MyTracer) Start(ctx context.Context, spanName string, opts ...trace2.SpanStartOption) (context.Context, trace2.Span) {
+ if m.Enable {
+ return m.tracer.Start(ctx, spanName, opts...)
+ }
+ return m.nopTracer.Start(ctx, spanName, opts...)
+}
diff --git a/webook/ioc/ranking.go b/webook/ioc/ranking.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c5d2e24f5e81e0e8384adf5c3f8df1264994c82
--- /dev/null
+++ b/webook/ioc/ranking.go
@@ -0,0 +1,27 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/job"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ rlock "github.com/gotomicro/redis-lock"
+ "github.com/robfig/cron/v3"
+ "time"
+)
+
+func InitRankingJob(svc service.RankingService,
+ rlockClient *rlock.Client,
+ l logger.LoggerV1) *job.RankingJob {
+ return job.NewRankingJob(svc, rlockClient, l, time.Second*30)
+}
+
+func InitJobs(l logger.LoggerV1, rankingJob *job.RankingJob) *cron.Cron {
+ res := cron.New(cron.WithSeconds())
+ cbd := job.NewCronJobBuilder(l)
+ // 这里每三分钟一次
+ _, err := res.AddJob("0 */3 * * * ?", cbd.Build(rankingJob))
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
diff --git a/webook/ioc/redis.go b/webook/ioc/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..3cad00b993346acd8734199175f4c4a6bec40684
--- /dev/null
+++ b/webook/ioc/redis.go
@@ -0,0 +1,24 @@
+package ioc
+
+import (
+ rlock "github.com/gotomicro/redis-lock"
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+)
+
+func InitRedis() redis.Cmdable {
+ addr := viper.GetString("redis.addr")
+ redisClient := redis.NewClient(&redis.Options{
+ Addr: addr,
+ })
+ return redisClient
+}
+
+func InitRLockClient(cmd redis.Cmdable) *rlock.Client {
+ return rlock.NewClient(cmd)
+}
+
+//
+//func NewRateLimiter() redis.Limiter {
+//
+//}
diff --git a/webook/ioc/sms.go b/webook/ioc/sms.go
new file mode 100644
index 0000000000000000000000000000000000000000..4b4b26cdfb6860b4e08ef9ce873523dedef93c84
--- /dev/null
+++ b/webook/ioc/sms.go
@@ -0,0 +1,17 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms"
+ "gitee.com/geekbang/basic-go/webook/internal/service/sms/memory"
+ "github.com/redis/go-redis/v9"
+)
+
+func InitSMSService(cmd redis.Cmdable) sms.Service {
+ // 换内存,还是换别的
+ //svc := ratelimit.NewRatelimitSMSService(memory.NewService(),
+ // limiter.NewRedisSlidingWindowLimiter(cmd, time.Second, 100))
+ //return retryable.NewService(svc, 3)
+ // 接入监控
+ //return metrics.NewPrometheusDecorator(memory.NewService())
+ return memory.NewService()
+}
diff --git a/webook/ioc/user.go b/webook/ioc/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..5ceadd830014a936931308683ee0808bcb7f1d87
--- /dev/null
+++ b/webook/ioc/user.go
@@ -0,0 +1,31 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/pkg/redisx"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/redis/go-redis/v9"
+)
+
+//func InitUserHandler(repo repository.UserRepository) service.UserService {
+// l, err := zap.NewDevelopment()
+// if err != nil {
+// panic(err)
+// }
+// return service.NewUserService(repo, )
+//}
+
+// InitUserCache 配合 PrometheusHook 使用
+func InitUserCache(client *redis.ClusterClient) cache.UserCache {
+ client.AddHook(redisx.NewPrometheusHook(
+ prometheus.SummaryOpts{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Name: "gin_http",
+ Help: "统计 GIN 的 HTTP 接口",
+ ConstLabels: map[string]string{
+ "biz": "user",
+ },
+ }))
+ panic("你别调用")
+}
diff --git a/webook/ioc/web.go b/webook/ioc/web.go
new file mode 100644
index 0000000000000000000000000000000000000000..e0be81a534050c9cc20b59cf479f0e7153c22ed7
--- /dev/null
+++ b/webook/ioc/web.go
@@ -0,0 +1,89 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/internal/web/middleware"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx/middlewares/metric"
+ logger2 "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/redis/go-redis/v9"
+ "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
+ "strings"
+ "time"
+)
+
+func InitWebServer(mdls []gin.HandlerFunc, userHdl *web.UserHandler,
+ oauth2WechatHdl *web.OAuth2WechatHandler, articleHdl *web.ArticleHandler) *gin.Engine {
+ server := gin.Default()
+ server.Use(mdls...)
+ userHdl.RegisterRoutes(server)
+ articleHdl.RegisterRoutes(server)
+ oauth2WechatHdl.RegisterRoutes(server)
+ (&web.ObservabilityHandler{}).RegisterRoutes(server)
+ return server
+}
+
+func InitMiddlewares(redisClient redis.Cmdable,
+ l logger2.LoggerV1,
+ jwtHdl ijwt.Handler) []gin.HandlerFunc {
+ //bd := logger.NewBuilder(func(ctx context.Context, al *logger.AccessLog) {
+ // l.Debug("HTTP请求", logger2.Field{Key: "al", Value: al})
+ //}).AllowReqBody(true).AllowRespBody()
+ //viper.OnConfigChange(func(in fsnotify.Event) {
+ // ok := viper.GetBool("web.logreq")
+ // bd.AllowReqBody(ok)
+ //})
+ ginx.InitCounter(prometheus.CounterOpts{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Name: "http_biz_code",
+ Help: "HTTP 的业务错误码",
+ })
+ return []gin.HandlerFunc{
+ corsHdl(),
+ (&metric.MiddlewareBuilder{
+ Namespace: "geekbang_daming",
+ Subsystem: "webook",
+ Name: "gin_http",
+ Help: "统计 GIN 的 HTTP 接口",
+ InstanceID: "my-instance-1",
+ }).Build(),
+ otelgin.Middleware("webook"),
+ //bd.Build(),
+ middleware.NewLoginJWTMiddlewareBuilder(jwtHdl).
+ IgnorePaths("/users/signup").
+ IgnorePaths("/users/refresh_token").
+ IgnorePaths("/users/login_sms/code/send").
+ IgnorePaths("/users/login_sms").
+ IgnorePaths("/oauth2/wechat/authurl").
+ IgnorePaths("/oauth2/wechat/callback").
+ IgnorePaths("/users/login").
+ IgnorePaths("/test/metric").
+ Build(),
+ //ratelimit.NewBuilder(redisClient, time.Second, 100).Build(),
+ }
+}
+
+func corsHdl() gin.HandlerFunc {
+ return cors.New(cors.Config{
+ //AllowOrigins: []string{"*"},
+ //AllowMethods: []string{"POST", "GET"},
+ AllowHeaders: []string{"Content-Type", "Authorization"},
+ // 你不加这个,前端是拿不到的
+ ExposeHeaders: []string{"x-jwt-token", "x-refresh-token"},
+ // 是否允许你带 cookie 之类的东西
+ AllowCredentials: true,
+ AllowOriginFunc: func(origin string) bool {
+ if strings.HasPrefix(origin, "http://localhost") {
+ // 你的开发环境
+ return true
+ }
+ return strings.Contains(origin, "yourcompany.com")
+ },
+ MaxAge: 12 * time.Hour,
+ })
+}
diff --git a/webook/ioc/wechat.go b/webook/ioc/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..18c5c25f7bcb53e2f0d8f3af41de64a5f14fcc90
--- /dev/null
+++ b/webook/ioc/wechat.go
@@ -0,0 +1,26 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/service/oauth2/wechat"
+ logger2 "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "os"
+)
+
+func InitWechatService(l logger2.LoggerV1) wechat.Service {
+ appId, ok := os.LookupEnv("WECHAT_APP_ID")
+ if !ok {
+ panic("没有找到环境变量 WECHAT_APP_ID ")
+ }
+ appKey, ok := os.LookupEnv("WECHAT_APP_SECRET")
+ if !ok {
+ panic("没有找到环境变量 WECHAT_APP_SECRET")
+ }
+ // 692jdHsogrsYqxaUK9fgxw
+ return wechat.NewService(appId, appKey, l)
+}
+
+//func NewWechatHandlerConfig() web.WechatHandlerConfig {
+// return web.WechatHandlerConfig{
+// Secure: false,
+// }
+//}
diff --git a/webook/k8s-ingress-nginx.yaml b/webook/k8s-ingress-nginx.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..18a23ce36a15e2230e15c84ee26715a31d4cdb3c
--- /dev/null
+++ b/webook/k8s-ingress-nginx.yaml
@@ -0,0 +1,22 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: webook-live-ingress
+spec:
+# 老子要用 nginx
+ ingressClassName: nginx
+ rules:
+# host 是 live.webook.com 的时候,命中我这条
+ - host: live.webook.com
+ http:
+ paths:
+# - 请求路径的前缀是 / 的时候
+# - 将流量转发过去后面的 webook-live 服务上
+# - 端口是 81
+ - backend:
+ service:
+ name: webook-live
+ port:
+ number: 81
+ pathType: Prefix
+ path: /
\ No newline at end of file
diff --git a/webook/k8s-mysql-deployment.yaml b/webook/k8s-mysql-deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0b4e10f04c3b3310d53fce34a4af1984623cd389
--- /dev/null
+++ b/webook/k8s-mysql-deployment.yaml
@@ -0,0 +1,44 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: webook-live-mysql
+ labels:
+ app: webook-live-mysql
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: webook-live-mysql
+ template:
+ metadata:
+ name: webook-live-mysql
+ labels:
+ app: webook-live-mysql
+ spec:
+ containers:
+ - name: webook-live-mysql
+ image: mysql:8.0
+ env:
+ - name: MYSQL_ROOT_PASSWORD
+ value: root
+ imagePullPolicy: IfNotPresent
+ volumeMounts:
+# - 这边要对应到 mysql 的数据存储的位置
+# - 通过 MySQL 的配置可以改这个目录
+ - mountPath: /var/lib/mysql
+# 我 POD 里面有那么多 volumes,我要用哪个
+ name: mysql-storage
+ ports:
+ - containerPort: 3306
+# - name: webook-live-hadoop
+ restartPolicy: Always
+# 我整个 POD 有哪些
+ volumes:
+ - name: mysql-storage
+ persistentVolumeClaim:
+ claimName: webook-mysql-live-claim-v3
+# - name: hadoop-storage
+# persistentVolumeClaim:
+# claimName: webook-hadoop-live-claim
+
+
\ No newline at end of file
diff --git a/webook/k8s-mysql-pv.yaml b/webook/k8s-mysql-pv.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a859a9b67a996f1a6efe854c92b5b786518c0e62
--- /dev/null
+++ b/webook/k8s-mysql-pv.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+# 这个指的是 我 k8s 有哪些 volume
+# 我 k8s 有什么????
+kind: PersistentVolume
+metadata:
+ name: my-local-pv-v3
+spec:
+ storageClassName: suibianv3
+ capacity:
+ storage: 1Gi
+ accessModes:
+ - ReadWriteOnce
+ hostPath:
+ path: "/mnt/live"
+
+
\ No newline at end of file
diff --git a/webook/k8s-mysql-pvc.yaml b/webook/k8s-mysql-pvc.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..688a6d81cc571b723d84dccba1929871a8e0ba8a
--- /dev/null
+++ b/webook/k8s-mysql-pvc.yaml
@@ -0,0 +1,22 @@
+# pvc => PersistentVolumeClaim
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+# 这个是指我 mysql 要用的东西
+ name: webook-mysql-live-claim-v3
+spec:
+# 这个可以随便
+ storageClassName: suibianv3
+ accessModes:
+# 一个人?一个线程?还是一个POD?还是一个数据库用户?读写
+ - ReadWriteOnce
+# 多个读,一个写
+# - ReadOnlyMany
+# - 多个读写
+# - ReadWriteMany
+ resources:
+ requests:
+# 1 GB
+ storage: 1Gi
+
+
\ No newline at end of file
diff --git a/webook/k8s-mysql-service.yaml b/webook/k8s-mysql-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..20c804dec45a10358f54378e3834963edf12360a
--- /dev/null
+++ b/webook/k8s-mysql-service.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: webook-live-mysql
+spec:
+ selector:
+ app: webook-live-mysql
+ ports:
+ - protocol: TCP
+# 你访问的端口
+ port: 11309
+# 作业
+# port: 3308
+ targetPort: 3306
+ nodePort: 30002
+ type: NodePort
+
\ No newline at end of file
diff --git a/webook/k8s-redis-deployment.yaml b/webook/k8s-redis-deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7f12329d6e9621e709f7538d2144f87c927eaf87
--- /dev/null
+++ b/webook/k8s-redis-deployment.yaml
@@ -0,0 +1,23 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: webook-live-redis
+ labels:
+ app: webook-live-redis
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: webook-live-redis
+ template:
+ metadata:
+ name: webook-live-redis
+ labels:
+ app: webook-live-redis
+ spec:
+ containers:
+ - name: webook-live-redis
+ image: redis:latest
+ imagePullPolicy: IfNotPresent
+ restartPolicy: Always
+
\ No newline at end of file
diff --git a/webook/k8s-redis-service.yaml b/webook/k8s-redis-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..38bdb01e6fb9ce36f37e6ea5e1d5845c4eaf04ef
--- /dev/null
+++ b/webook/k8s-redis-service.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: webook-live-redis
+spec:
+ selector:
+ app: webook-live-redis
+ ports:
+ - protocol: TCP
+# 作业
+# port: 6380
+ port: 11479
+# Redis 默认端口
+ targetPort: 6379
+ nodePort: 30003
+ type: NodePort
\ No newline at end of file
diff --git a/webook/k8s-webook-deployment.yaml b/webook/k8s-webook-deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0538e44c232eb54019ec47c6154f7568a68d5f87
--- /dev/null
+++ b/webook/k8s-webook-deployment.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: webook-live
+# specification
+spec:
+# 副本数量
+# 作业
+# replicas: 2
+ replicas: 3
+ selector:
+ matchLabels:
+ app: webook-live
+# template 描述的是你的 POD 是什么样的
+ template:
+ metadata:
+ labels:
+ app: webook-live
+# POD 的具体信息
+ spec:
+ containers:
+ - name: webook
+ image: flycash/webook-live:v0.0.1
+ ports:
+# - 作业
+# - containerPort: 8081
+ - containerPort: 8080
diff --git a/webook/k8s-webook-service.yaml b/webook/k8s-webook-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4856e258f7dc52aa5e42202b78767ee749f08f52
--- /dev/null
+++ b/webook/k8s-webook-service.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: webook-live
+spec:
+# ClusterIP
+ type: LoadBalancer
+ selector:
+ app: webook-live
+ ports:
+ - protocol: TCP
+ name: http
+ port: 81
+# 作业
+# targetPort: 8081
+ targetPort: 8080
+
+
\ No newline at end of file
diff --git a/webook/main.go b/webook/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..452480f18e47b9cb1fe15d41caeddd88110b6ea4
--- /dev/null
+++ b/webook/main.go
@@ -0,0 +1,192 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/fsnotify/fsnotify"
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+ _ "github.com/spf13/viper/remote"
+ "go.uber.org/zap"
+ "net/http"
+ "time"
+)
+
+func main() {
+ //db := initDB()
+ //rdb := initRedis()
+ //
+ //server := initWebServer()
+ //
+ //u := initUser(db, rdb)
+ //u.RegisterRoutes(server)
+
+ //initViperRemote()
+ initViperV1()
+ initLogger()
+
+ closeFunc := ioc.InitOTEL()
+ initPrometheus()
+ keys := viper.AllKeys()
+ println(keys)
+ //setting := viper.AllSettings()
+ //fmt.Println(setting)
+ app := InitWebServer()
+ // Consumer 在我设计下,类似于 Web,或者 GRPC 之类的,是一个顶级入口
+ for _, c := range app.consumers {
+ err := c.Start()
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ app.cron.Start()
+
+ server := app.web
+ server.GET("/hello", func(ctx *gin.Context) {
+ ctx.String(http.StatusOK, "你好,你来了")
+ })
+
+ server.Run(":8080")
+ // 一分钟内你要关完,要退出
+ ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+ defer cancel()
+ closeFunc(ctx)
+
+ //httpServer := http.Server{
+ // Addr: ":8080",
+ // Handler: app.web.Handler(),
+ //}
+ //httpServer.ListenAndServe()
+ //httpServer.Shutdown(context.Background())
+
+ // gin 是对这个的二次封装
+ // http.Server{}.Shutdown()
+ ctx = app.cron.Stop()
+ // 想办法 close ??
+ // 这边可以考虑超时强制退出,防止有些任务,执行特别长的时间
+ // 这边就是退出之前 sleep 了一下
+ tm := time.NewTimer(time.Minute * 10)
+ select {
+ case <-tm.C:
+ case <-ctx.Done():
+ }
+ // 作业
+ //server.Run(":8081")
+}
+
+func initPrometheus() {
+ go func() {
+ http.Handle("/metrics", promhttp.Handler())
+ http.ListenAndServe(":8081", nil)
+ }()
+}
+
+func initLogger() {
+ logger, err := zap.NewDevelopment()
+ if err != nil {
+ panic(err)
+ }
+ zap.L().Info("这是 replace 之前")
+ // 如果你不 replace,直接用 zap.L(),你啥都打不出来。
+ zap.ReplaceGlobals(logger)
+ zap.L().Info("hello,你搞好了")
+
+ type Demo struct {
+ Name string `json:"name"`
+ }
+ zap.L().Info("这是实验参数",
+ zap.Error(errors.New("这是一个 error")),
+ zap.Int64("id", 123),
+ zap.Any("一个结构体", Demo{Name: "hello"}))
+}
+
+func initViperReader() {
+ viper.SetConfigType("yaml")
+ cfg := `
+db.mysql:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+
+redis:
+ addr: "localhost:6379"
+`
+ err := viper.ReadConfig(bytes.NewReader([]byte(cfg)))
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperRemote() {
+ err := viper.AddRemoteProvider("etcd3",
+ // 通过 webook 和其他使用 etcd 的区别出来
+ "http://127.0.0.1:12379", "/webook")
+ if err != nil {
+ panic(err)
+ }
+ viper.SetConfigType("yaml")
+ err = viper.WatchRemoteConfig()
+ if err != nil {
+ panic(err)
+ }
+ viper.OnConfigChange(func(in fsnotify.Event) {
+ fmt.Println(in.Name, in.Op)
+ })
+ err = viper.ReadRemoteConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV1() {
+ cfile := pflag.String("config",
+ "config/config.yaml", "指定配置文件路径")
+ pflag.Parse()
+ viper.SetConfigFile(*cfile)
+ // 实时监听配置变更
+ viper.WatchConfig()
+ // 只能告诉你文件变了,不能告诉你,文件的哪些内容变了
+ viper.OnConfigChange(func(in fsnotify.Event) {
+ // 比较好的设计,它会在 in 里面告诉你变更前的数据,和变更后的数据
+ // 更好的设计是,它会直接告诉你差异。
+ fmt.Println(in.Name, in.Op)
+ fmt.Println(viper.GetString("db.dsn"))
+ })
+ //viper.SetDefault("db.mysql.dsn",
+ // "root:root@tcp(localhost:3306)/mysql")
+ //viper.SetConfigFile("config/dev.yaml")
+ //viper.KeyDelimiter("-")
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViper() {
+ viper.SetDefault("db.mysql.dsn",
+ "root:root@tcp(localhost:3306)/mysql")
+ // 配置文件的名字,但是不包含文件扩展名
+ // 不包含 .go, .yaml 之类的后缀
+ viper.SetConfigName("dev")
+ // 告诉 viper 我的配置用的是 yaml 格式
+ // 现实中,有很多格式,JSON,XML,YAML,TOML,ini
+ viper.SetConfigType("yaml")
+ // 当前工作目录下的 config 子目录
+ viper.AddConfigPath("./config")
+ //viper.AddConfigPath("/tmp/config")
+ //viper.AddConfigPath("/etc/webook")
+ // 读取配置到 viper 里面,或者你可以理解为加载到内存里面
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+
+ //otherViper := viper.New()
+ //otherViper.SetConfigName("myjson")
+ //otherViper.AddConfigPath("./config")
+ //otherViper.SetConfigType("json")
+}
diff --git a/webook/payment/config/dev.yaml b/webook/payment/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5ec8c70434131f14f00d644a44446965b7709382
--- /dev/null
+++ b/webook/payment/config/dev.yaml
@@ -0,0 +1,19 @@
+http:
+ addr: ":8070"
+
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook_payment"
+
+kafka:
+ addrs:
+ - "localhost:9094"
+
+etcd:
+ endpoints:
+ - "localhost:12379"
+
+grpc:
+ server:
+ port: 8098
+ etcdAddr: "localhost:12379"
+ etcdTTL: 60
diff --git a/webook/payment/domain/payment.go b/webook/payment/domain/payment.go
new file mode 100644
index 0000000000000000000000000000000000000000..2d26a8e38d152720ef4b121c5728599ad99beeca
--- /dev/null
+++ b/webook/payment/domain/payment.go
@@ -0,0 +1,46 @@
+package domain
+
+import "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
+
+type Amount struct {
+ // 如果要支持国际化,那么这个是不能少的
+ Currency string
+ // 这里我们遵循微信的做法,就用 int64 来记录分数。
+ // 那么对于不同的货币来说,这个字段的含义就不同。
+ // 比如说一些货币没有分,只有整数。
+ Total int64
+}
+
+type Payment struct {
+ Amt Amount
+ // 代表业务,业务方决定怎么生成,
+ // 我们这边不管。
+ BizTradeNO string
+ // 订单本身的描述
+ Description string
+ Status PaymentStatus
+ // 第三方那边返回的 ID
+ TxnID string
+}
+
+type PaymentStatus uint8
+
+func (s PaymentStatus) AsUint8() uint8 {
+ return uint8(s)
+}
+
+const (
+ PaymentStatusUnknown = iota
+ PaymentStatusInit
+ PaymentStatusSuccess
+ PaymentStatusFailed
+ PaymentStatusRefund
+
+ //PaymentStatusRefundFail
+ //PaymentStatusRefundSuccess
+ // PaymentStatusRecoup
+ // PaymentStatusRecoupFailed
+ // PaymentStatusRecoupSuccess
+)
+
+type Txn = payments.Transaction
diff --git a/webook/payment/events/producer.go b/webook/payment/events/producer.go
new file mode 100644
index 0000000000000000000000000000000000000000..5817ed44f0a4e2cb1874fb3b462198b4a207e414
--- /dev/null
+++ b/webook/payment/events/producer.go
@@ -0,0 +1,7 @@
+package events
+
+import "context"
+
+type Producer interface {
+ ProducePaymentEvent(ctx context.Context, evt PaymentEvent) error
+}
diff --git a/webook/payment/events/sarama_producer.go b/webook/payment/events/sarama_producer.go
new file mode 100644
index 0000000000000000000000000000000000000000..876bec3d6b29e774d29f6f8765139dbd52024a9c
--- /dev/null
+++ b/webook/payment/events/sarama_producer.go
@@ -0,0 +1,34 @@
+package events
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/IBM/sarama"
+)
+
+type SaramaProducer struct {
+ producer sarama.SyncProducer
+}
+
+func NewSaramaProducer(client sarama.Client) (*SaramaProducer, error) {
+ p, err := sarama.NewSyncProducerFromClient(client)
+ if err != nil {
+ return nil, err
+ }
+ return &SaramaProducer{
+ p,
+ }, nil
+}
+
+func (s *SaramaProducer) ProducePaymentEvent(ctx context.Context, evt PaymentEvent) error {
+ data, err := json.Marshal(evt)
+ if err != nil {
+ return err
+ }
+ _, _, err = s.producer.SendMessage(&sarama.ProducerMessage{
+ Key: sarama.StringEncoder(evt.BizTradeNO),
+ Topic: evt.Topic(),
+ Value: sarama.ByteEncoder(data),
+ })
+ return err
+}
diff --git a/webook/payment/events/types.go b/webook/payment/events/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..d95d6e60461baf1811f834a7e5aae0cf34083e05
--- /dev/null
+++ b/webook/payment/events/types.go
@@ -0,0 +1,18 @@
+package events
+
+// PaymentEvent 也是最简设计
+// 有一些人会习惯把支付详情也放进来,但是目前来看是没有必要的
+// 后续如果要接入大数据之类的,那么就可以考虑提供 payment 详情
+type PaymentEvent struct {
+ BizTradeNO string
+ Status uint8
+
+ // TxnId,时间戳巴拉巴拉
+ // 全部字段在这里
+ // Detail string
+}
+
+func (PaymentEvent) Topic() string {
+ // return biz + "_payment_events"
+ return "payment_events"
+}
diff --git a/webook/payment/grpc/wechat_native.go b/webook/payment/grpc/wechat_native.go
new file mode 100644
index 0000000000000000000000000000000000000000..6138e5367ba6315e2c0ad24f521f199169a8acbc
--- /dev/null
+++ b/webook/payment/grpc/wechat_native.go
@@ -0,0 +1,59 @@
+package grpc
+
+import (
+ "context"
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "google.golang.org/grpc"
+)
+
+type WechatServiceServer struct {
+ pmtv1.UnimplementedWechatPaymentServiceServer
+ svc *wechat.NativePaymentService
+}
+
+func NewWechatServiceServer(svc *wechat.NativePaymentService) *WechatServiceServer {
+ return &WechatServiceServer{svc: svc}
+}
+
+func (s *WechatServiceServer) Register(server *grpc.Server) {
+ pmtv1.RegisterWechatPaymentServiceServer(server, s)
+}
+
+func (s *WechatServiceServer) GetPayment(ctx context.Context, req *pmtv1.GetPaymentRequest) (*pmtv1.GetPaymentResponse, error) {
+ p, err := s.svc.GetPayment(ctx, req.GetBizTradeNo())
+ if err != nil {
+ return nil, err
+ }
+ return &pmtv1.GetPaymentResponse{
+ Status: pmtv1.PaymentStatus(p.Status),
+ }, nil
+}
+
+// 根据 type 来分发
+//func (s *WechatServiceServer) NativePrePay(ctx context.Context, request *pmtv1.PrePayRequest) (*pmtv1.NativePrePayResponse, error) {
+// switch request.Type {
+// case "native":
+// return s.svc.Prepay()
+// case "jsapi":
+// // 掉另外一个方法
+// }
+//}
+
+func (s *WechatServiceServer) NativePrePay(ctx context.Context, request *pmtv1.PrePayRequest) (*pmtv1.NativePrePayResponse, error) {
+ codeURL, err := s.svc.Prepay(ctx, domain.Payment{
+ Amt: domain.Amount{
+ Currency: request.Amt.Currency,
+ Total: request.Amt.Total,
+ },
+ BizTradeNO: request.BizTradeNo,
+ Description: request.Description,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pmtv1.NativePrePayResponse{
+ CodeUrl: codeURL,
+ }, nil
+}
diff --git a/webook/payment/integration/native_service_test.go b/webook/payment/integration/native_service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b32f20fee67dd6198d9050a2bc60f9bcc1bee2c7
--- /dev/null
+++ b/webook/payment/integration/native_service_test.go
@@ -0,0 +1,88 @@
+package integration
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gitee.com/geekbang/basic-go/webook/payment/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+ "testing"
+ "time"
+)
+
+type WechatNativeServiceTestSuite struct {
+ suite.Suite
+ svc *wechat.NativePaymentService
+ db *gorm.DB
+}
+
+func TestWechatNativeService(t *testing.T) {
+ suite.Run(t, new(WechatNativeServiceTestSuite))
+}
+
+func (s *WechatNativeServiceTestSuite) SetupSuite() {
+ s.svc = startup.InitWechatNativeService()
+ s.db = startup.InitTestDB()
+}
+
+func (s *WechatNativeServiceTestSuite) TearDownTest() {
+ s.db.Exec("TRUNCATE TABLE `payments`")
+}
+
+// 记得配置各个环境变量
+func (s *WechatNativeServiceTestSuite) TestPrepay() {
+ bizNo1 := "integration-1234-p"
+ testCases := []struct {
+ name string
+ pmt domain.Payment
+ after func(t *testing.T)
+ wantErr error
+ }{
+ {
+ name: "获得了code_url",
+ pmt: domain.Payment{
+ Amt: domain.Amount{
+ Total: 1,
+ Currency: "CNY",
+ },
+ BizTradeNO: bizNo1,
+ Description: "我在这边买了一个产品",
+ },
+ after: func(t *testing.T) {
+ var pmt dao.Payment
+ err := s.db.Where("biz_trade_no = ?", bizNo1).First(&pmt).Error
+ require.NoError(t, err)
+ assert.True(t, pmt.Id > 0)
+ pmt.Id = 0
+ assert.True(t, pmt.Ctime > 0)
+ pmt.Ctime = 0
+ assert.True(t, pmt.Utime > 0)
+ pmt.Utime = 0
+ assert.Equal(t, dao.Payment{
+ Amt: 1,
+ Currency: "CNY",
+ BizTradeNO: bizNo1,
+ Description: "我在这边买了一个产品",
+ Status: domain.PaymentStatusInit,
+ }, pmt)
+ },
+ },
+ }
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ url, err := s.svc.Prepay(ctx, tc.pmt)
+ assert.Equal(t, tc.wantErr, err)
+ if tc.wantErr == nil {
+ assert.NotEmpty(t, url)
+ t.Log(url)
+ }
+ tc.after(t)
+ })
+ }
+}
diff --git a/webook/payment/integration/startup/db.go b/webook/payment/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..64ff47273347c601916a6020a1379f3d010c4859
--- /dev/null
+++ b/webook/payment/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook_payment"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/payment/integration/startup/wire.go b/webook/payment/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bb3aad4241d1c3dee68ff4ee8e0bdbe05719565
--- /dev/null
+++ b/webook/payment/integration/startup/wire.go
@@ -0,0 +1,25 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/ioc"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "github.com/google/wire"
+)
+
+var thirdPartySet = wire.NewSet(ioc.InitLogger, InitTestDB)
+
+var wechatNativeSvcSet = wire.NewSet(
+ ioc.InitWechatClient,
+ dao.NewPaymentGORMDAO,
+ repository.NewPaymentRepository,
+ ioc.InitWechatNativeService,
+ ioc.InitWechatConfig)
+
+func InitWechatNativeService() *wechat.NativePaymentService {
+ wire.Build(wechatNativeSvcSet, thirdPartySet)
+ return new(wechat.NativePaymentService)
+}
diff --git a/webook/payment/integration/startup/wire_gen.go b/webook/payment/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..340d05963baa1db7fa68ec0586723312de3b8ed3
--- /dev/null
+++ b/webook/payment/integration/startup/wire_gen.go
@@ -0,0 +1,34 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/ioc"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitWechatNativeService() *wechat.NativePaymentService {
+ wechatConfig := ioc.InitWechatConfig()
+ client := ioc.InitWechatClient(wechatConfig)
+ gormDB := InitTestDB()
+ paymentDAO := dao.NewPaymentGORMDAO(gormDB)
+ paymentRepository := repository.NewPaymentRepository(paymentDAO)
+ loggerV1 := ioc.InitLogger()
+ nativePaymentService := ioc.InitWechatNativeService(client, paymentRepository, loggerV1, wechatConfig)
+ return nativePaymentService
+}
+
+// wire.go:
+
+var thirdPartySet = wire.NewSet(ioc.InitLogger, InitTestDB)
+
+var wechatNativeSvcSet = wire.NewSet(ioc.InitWechatClient, dao.NewPaymentGORMDAO, repository.NewPaymentRepository, ioc.InitWechatNativeService, ioc.InitWechatConfig)
diff --git a/webook/payment/ioc/db.go b/webook/payment/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..ace881032c91bf04b1a69db71ad1440edad0cfe2
--- /dev/null
+++ b/webook/payment/ioc/db.go
@@ -0,0 +1,31 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func InitDB() *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v1, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{})
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
diff --git a/webook/payment/ioc/etcd.go b/webook/payment/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/payment/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/payment/ioc/grpc.go b/webook/payment/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..079de95dca4cfeb04e68999f08443b320a256efe
--- /dev/null
+++ b/webook/payment/ioc/grpc.go
@@ -0,0 +1,36 @@
+package ioc
+
+import (
+ grpc2 "gitee.com/geekbang/basic-go/webook/payment/grpc"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCServer(wesvc *grpc2.WechatServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer(grpc.ChainUnaryInterceptor(
+ //logger.NewLoggerInterceptorBuilder(l).BuildUnaryServerInterceptor(),
+ ))
+ wesvc.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "payment",
+ L: l,
+ EtcdTTL: cfg.EtcdTTL,
+ EtcdClient: ecli,
+ }
+}
diff --git a/webook/payment/ioc/kafka.go b/webook/payment/ioc/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..45a2ccfbea0788d02fd6ac327f3086e69bfc38a5
--- /dev/null
+++ b/webook/payment/ioc/kafka.go
@@ -0,0 +1,34 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/events"
+ "github.com/IBM/sarama"
+ "github.com/spf13/viper"
+)
+
+func InitKafka() sarama.Client {
+ type Config struct {
+ Addrs []string `yaml:"addrs"`
+ }
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ saramaCfg.Producer.Partitioner = sarama.NewConsistentCRCHashPartitioner
+ var cfg Config
+ err := viper.UnmarshalKey("kafka", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := sarama.NewClient(cfg.Addrs, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+func InitProducer(client sarama.Client) events.Producer {
+ res, err := events.NewSaramaProducer(client)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
diff --git a/webook/payment/ioc/log.go b/webook/payment/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/payment/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/payment/ioc/web.go b/webook/payment/ioc/web.go
new file mode 100644
index 0000000000000000000000000000000000000000..18bb293d1d5b119c046c8fd2334cd2404ad66975
--- /dev/null
+++ b/webook/payment/ioc/web.go
@@ -0,0 +1,24 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/web"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/spf13/viper"
+)
+
+func InitGinServer(hdl *web.WechatHandler) *ginx.Server {
+ engine := gin.Default()
+ hdl.RegisterRoutes(engine)
+ addr := viper.GetString("http.addr")
+ ginx.InitCounter(prometheus.CounterOpts{
+ Namespace: "daming_geektime",
+ Subsystem: "webook_payment",
+ Name: "http",
+ })
+ return &ginx.Server{
+ Engine: engine,
+ Addr: addr,
+ }
+}
diff --git a/webook/payment/ioc/wechat.go b/webook/payment/ioc/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f002f907562b097bbedb818dfbe3795a7747d8c
--- /dev/null
+++ b/webook/payment/ioc/wechat.go
@@ -0,0 +1,86 @@
+package ioc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/events"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/wechatpay-apiv3/wechatpay-go/core"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/notify"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/option"
+ "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
+ "github.com/wechatpay-apiv3/wechatpay-go/utils"
+ "os"
+)
+
+func InitWechatClient(cfg WechatConfig) *core.Client {
+
+ // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
+ mchPrivateKey, err := utils.LoadPrivateKeyWithPath(
+ // 注意这个文件我没有上传,所以你需要准备一个
+ cfg.KeyPath,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ ctx := context.Background()
+ // 使用商户私钥等初始化 client
+ client, err := core.NewClient(
+ ctx,
+ option.WithWechatPayAutoAuthCipher(
+ cfg.MchID, cfg.MchSerialNum,
+ mchPrivateKey, cfg.MchKey),
+ )
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+func InitWechatNativeService(
+ cli *core.Client,
+ repo repository.PaymentRepository,
+ l logger.LoggerV1,
+ producer events.Producer,
+ cfg WechatConfig) *wechat.NativePaymentService {
+ return wechat.NewNativePaymentService(&native.NativeApiService{
+ Client: cli,
+ }, repo, producer, l, cfg.AppID, cfg.MchID)
+}
+
+func InitWechatNotifyHandler(cfg WechatConfig) *notify.Handler {
+ certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(cfg.MchID)
+ // 3. 使用apiv3 key、证书访问器初始化 `notify.Handler`
+ handler, err := notify.NewRSANotifyHandler(cfg.MchKey,
+ verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
+ if err != nil {
+ panic(err)
+ }
+ return handler
+}
+
+func InitWechatConfig() WechatConfig {
+ return WechatConfig{
+ AppID: os.Getenv("WEPAY_APP_ID"),
+ MchID: os.Getenv("WEPAY_MCH_ID"),
+ MchKey: os.Getenv("WEPAY_MCH_KEY"),
+ MchSerialNum: os.Getenv("WEPAY_MCH_SERIAL_NUM"),
+ CertPath: "./config/cert/apiclient_cert.pem",
+ KeyPath: "./config/cert/apiclient_key.pem",
+ }
+}
+
+type WechatConfig struct {
+ AppID string
+ MchID string
+ MchKey string
+ MchSerialNum string
+
+ // 证书
+ CertPath string
+ KeyPath string
+}
diff --git a/webook/payment/job/sync_wechat_order.go b/webook/payment/job/sync_wechat_order.go
new file mode 100644
index 0000000000000000000000000000000000000000..2c65cba1946abc3934e06180a170850d3bf99d02
--- /dev/null
+++ b/webook/payment/job/sync_wechat_order.go
@@ -0,0 +1,56 @@
+package job
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "time"
+)
+
+type SyncWechatOrderJob struct {
+ svc *wechat.NativePaymentService
+ l logger.LoggerV1
+}
+
+func (s *SyncWechatOrderJob) Name() string {
+ return "sync_wechat_order_job"
+}
+
+// Run 怎么调度。你可以考虑。间隔一分钟执行一次
+func (s *SyncWechatOrderJob) Run() error {
+ offset := 0
+ // 也可以做成参数
+ const limit = 100
+ // 三十分钟之前的订单我们就认为已经过期了。
+
+ // 如果你们的产品经理,或者老板要求快速对账
+
+ now := time.Now().Add(-time.Minute * 30)
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ pmts, err := s.svc.FindExpiredPayment(ctx, offset, limit, now)
+ cancel()
+ if err != nil {
+ // 直接中断,你也可以仔细区别不同错误
+ return err
+ }
+ // 因为微信没有批量接口,所以我们这里也只能单个查询
+ for _, pmt := range pmts {
+ // 单个重新设置超时
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second)
+ err = s.svc.SyncWechatInfo(ctx, pmt.BizTradeNO)
+ if err != nil {
+ // 这里你也可以中断,不过我个人倾向于处理完毕
+ s.l.Error("同步微信支付信息失败",
+ logger.String("trade_no", pmt.BizTradeNO),
+ logger.Error(err))
+ }
+ cancel()
+ }
+ if len(pmts) < limit {
+ // 没数据了
+ return nil
+ }
+ offset = offset + len(pmts)
+ }
+}
diff --git a/webook/payment/main.go b/webook/payment/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..ef377761557133d949e4df71faf9fc7b06563f50
--- /dev/null
+++ b/webook/payment/main.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := InitApp()
+ go func() {
+ err := app.GRPCServer.Serve()
+ panic(err)
+ }()
+ err := app.WebServer.Start()
+ panic(err)
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/webook/payment/repository/dao/gorm.go b/webook/payment/repository/dao/gorm.go
new file mode 100644
index 0000000000000000000000000000000000000000..e9804812489db96804bcab6c48a55122e22eb5dc
--- /dev/null
+++ b/webook/payment/repository/dao/gorm.go
@@ -0,0 +1,52 @@
+package dao
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gorm.io/gorm"
+ "time"
+)
+
+type PaymentGORMDAO struct {
+ db *gorm.DB
+}
+
+func (p *PaymentGORMDAO) GetPayment(ctx context.Context, bizTradeNO string) (Payment, error) {
+ var res Payment
+ err := p.db.WithContext(ctx).Where("biz_trade_no = ?", bizTradeNO).First(&res).Error
+ return res, err
+}
+
+func (p *PaymentGORMDAO) FindExpiredPayment(
+ ctx context.Context,
+ offset int, limit int, t time.Time) ([]Payment, error) {
+ var res []Payment
+ err := p.db.WithContext(ctx).Where("status = ? AND utime < ?",
+ // 我的 IDE 有问题,AsUint8 会报错
+ uint8(domain.PaymentStatusInit), t.UnixMilli()).
+ Offset(offset).Limit(limit).Find(&res).Error
+ return res, err
+}
+
+func (p *PaymentGORMDAO) UpdateTxnIDAndStatus(ctx context.Context,
+ bizTradeNo string,
+ txnID string, status domain.PaymentStatus) error {
+ return p.db.WithContext(ctx).Model(&Payment{}).
+ Where("biz_trade_no = ?", bizTradeNo).
+ Updates(map[string]any{
+ "txn_id": txnID,
+ "status": status.AsUint8(),
+ "utime": time.Now().UnixMilli(),
+ }).Error
+}
+
+func NewPaymentGORMDAO(db *gorm.DB) PaymentDAO {
+ return &PaymentGORMDAO{db: db}
+}
+
+func (p *PaymentGORMDAO) Insert(ctx context.Context, pmt Payment) error {
+ now := time.Now().UnixMilli()
+ pmt.Utime = now
+ pmt.Ctime = now
+ return p.db.WithContext(ctx).Create(&pmt).Error
+}
diff --git a/webook/payment/repository/dao/init.go b/webook/payment/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..75bff5e3050ce473fe8479693679d94821bb8edb
--- /dev/null
+++ b/webook/payment/repository/dao/init.go
@@ -0,0 +1,7 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(&Payment{})
+}
diff --git a/webook/payment/repository/dao/types.go b/webook/payment/repository/dao/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..cfe639eb5fc2aac7f8195b458062e784cf75df63
--- /dev/null
+++ b/webook/payment/repository/dao/types.go
@@ -0,0 +1,44 @@
+package dao
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "time"
+)
+
+type PaymentDAO interface {
+ Insert(ctx context.Context, pmt Payment) error
+ UpdateTxnIDAndStatus(ctx context.Context, bizTradeNo string, txnID string, status domain.PaymentStatus) error
+ FindExpiredPayment(ctx context.Context, offset int, limit int, t time.Time) ([]Payment, error)
+ GetPayment(ctx context.Context, bizTradeNO string) (Payment, error)
+}
+
+type Payment struct {
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ Amt int64
+ // 你存储枚举也可以,比如说 0-CNY
+ // 目前磁盘内存那么便宜,直接放 string 也可以
+ Currency string
+ // 可以抽象认为,这是一个简短的描述
+ // 也就是说即便是别的支付方式,这边也可以提供一个简单的描述
+ // 你可以认为这算是冗余的数据,因为从原则上来说,我们可以完全不保存的。
+ // 而是要求调用者直接 BizID 和 Biz 去找业务方要
+ // 管得越少,系统越稳
+ Description string `gorm:"description"`
+ // 后续可以考虑增加字段,来标记是用的是微信支付亦或是支付宝支付
+ // 也可以考虑提供一个巨大的 BLOB 字段,
+ // 来存储和支付有关的其它字段
+ // ExtraData
+
+ // 业务方传过来的
+ BizTradeNO string `gorm:"column:biz_trade_no;type:varchar(256);unique"`
+
+ // 第三方支付平台的事务 ID,唯一的
+ TxnID sql.NullString `gorm:"column:txn_id;type:varchar(128);unique"`
+
+ Status uint8
+ // Utime 上面要创建一个索引
+ Utime int64 `gorm:"index"`
+ Ctime int64
+}
diff --git a/webook/payment/repository/mocks/payment.mock.go b/webook/payment/repository/mocks/payment.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..c7d34f163298d7a2f45e95573dfda8bd82506397
--- /dev/null
+++ b/webook/payment/repository/mocks/payment.mock.go
@@ -0,0 +1,99 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: types.go
+//
+// Generated by this command:
+//
+// mockgen -source=types.go -destination=mocks/payment.mock.go --package=repomocks PaymentRepository
+//
+// Package repomocks is a generated GoMock package.
+package repomocks
+
+import (
+ context "context"
+ reflect "reflect"
+ time "time"
+
+ domain "gitee.com/geekbang/basic-go/webook/payment/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockPaymentRepository is a mock of PaymentRepository interface.
+type MockPaymentRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockPaymentRepositoryMockRecorder
+}
+
+// MockPaymentRepositoryMockRecorder is the mock recorder for MockPaymentRepository.
+type MockPaymentRepositoryMockRecorder struct {
+ mock *MockPaymentRepository
+}
+
+// NewMockPaymentRepository creates a new mock instance.
+func NewMockPaymentRepository(ctrl *gomock.Controller) *MockPaymentRepository {
+ mock := &MockPaymentRepository{ctrl: ctrl}
+ mock.recorder = &MockPaymentRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockPaymentRepository) EXPECT() *MockPaymentRepositoryMockRecorder {
+ return m.recorder
+}
+
+// AddPayment mocks base method.
+func (m *MockPaymentRepository) AddPayment(ctx context.Context, pmt domain.Payment) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddPayment", ctx, pmt)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// AddPayment indicates an expected call of AddPayment.
+func (mr *MockPaymentRepositoryMockRecorder) AddPayment(ctx, pmt any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPayment", reflect.TypeOf((*MockPaymentRepository)(nil).AddPayment), ctx, pmt)
+}
+
+// FindExpiredPayment mocks base method.
+func (m *MockPaymentRepository) FindExpiredPayment(ctx context.Context, offset, limit int, t time.Time) ([]domain.Payment, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FindExpiredPayment", ctx, offset, limit, t)
+ ret0, _ := ret[0].([]domain.Payment)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FindExpiredPayment indicates an expected call of FindExpiredPayment.
+func (mr *MockPaymentRepositoryMockRecorder) FindExpiredPayment(ctx, offset, limit, t any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindExpiredPayment", reflect.TypeOf((*MockPaymentRepository)(nil).FindExpiredPayment), ctx, offset, limit, t)
+}
+
+// GetPayment mocks base method.
+func (m *MockPaymentRepository) GetPayment(ctx context.Context, bizTradeNO string) (domain.Payment, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPayment", ctx, bizTradeNO)
+ ret0, _ := ret[0].(domain.Payment)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPayment indicates an expected call of GetPayment.
+func (mr *MockPaymentRepositoryMockRecorder) GetPayment(ctx, bizTradeNO any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPayment", reflect.TypeOf((*MockPaymentRepository)(nil).GetPayment), ctx, bizTradeNO)
+}
+
+// UpdatePayment mocks base method.
+func (m *MockPaymentRepository) UpdatePayment(ctx context.Context, pmt domain.Payment) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdatePayment", ctx, pmt)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdatePayment indicates an expected call of UpdatePayment.
+func (mr *MockPaymentRepositoryMockRecorder) UpdatePayment(ctx, pmt any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePayment", reflect.TypeOf((*MockPaymentRepository)(nil).UpdatePayment), ctx, pmt)
+}
diff --git a/webook/payment/repository/payment.go b/webook/payment/repository/payment.go
new file mode 100644
index 0000000000000000000000000000000000000000..5ac9dc66e1dcc37308c259cee504c8fa4881c39c
--- /dev/null
+++ b/webook/payment/repository/payment.go
@@ -0,0 +1,66 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "time"
+)
+
+type paymentRepository struct {
+ dao dao.PaymentDAO
+}
+
+func (p *paymentRepository) GetPayment(ctx context.Context, bizTradeNO string) (domain.Payment, error) {
+ r, err := p.dao.GetPayment(ctx, bizTradeNO)
+ return p.toDomain(r), err
+}
+
+func (p *paymentRepository) FindExpiredPayment(ctx context.Context, offset int, limit int, t time.Time) ([]domain.Payment, error) {
+ pmts, err := p.dao.FindExpiredPayment(ctx, offset, limit, t)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]domain.Payment, 0, len(pmts))
+ for _, pmt := range pmts {
+ res = append(res, p.toDomain(pmt))
+ }
+ return res, nil
+}
+
+func (p *paymentRepository) AddPayment(ctx context.Context, pmt domain.Payment) error {
+ return p.dao.Insert(ctx, p.toEntity(pmt))
+}
+
+func (p *paymentRepository) toDomain(pmt dao.Payment) domain.Payment {
+ return domain.Payment{
+ Amt: domain.Amount{
+ Currency: pmt.Currency,
+ Total: pmt.Amt,
+ },
+ BizTradeNO: pmt.BizTradeNO,
+ Description: pmt.Description,
+ Status: domain.PaymentStatus(pmt.Status),
+ TxnID: pmt.TxnID.String,
+ }
+}
+
+func (p *paymentRepository) toEntity(pmt domain.Payment) dao.Payment {
+ return dao.Payment{
+ Amt: pmt.Amt.Total,
+ Currency: pmt.Amt.Currency,
+ BizTradeNO: pmt.BizTradeNO,
+ Description: pmt.Description,
+ Status: domain.PaymentStatusInit,
+ }
+}
+
+func (p *paymentRepository) UpdatePayment(ctx context.Context, pmt domain.Payment) error {
+ return p.dao.UpdateTxnIDAndStatus(ctx, pmt.BizTradeNO, pmt.TxnID, pmt.Status)
+}
+
+func NewPaymentRepository(d dao.PaymentDAO) PaymentRepository {
+ return &paymentRepository{
+ dao: d,
+ }
+}
diff --git a/webook/payment/repository/types.go b/webook/payment/repository/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..50bff2d331107411f19a3b8bd23e6b604e365ee5
--- /dev/null
+++ b/webook/payment/repository/types.go
@@ -0,0 +1,16 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "time"
+)
+
+//go:generate mockgen -source=types.go -destination=mocks/payment.mock.go --package=repomocks PaymentRepository
+type PaymentRepository interface {
+ AddPayment(ctx context.Context, pmt domain.Payment) error
+ // UpdatePayment 这个设计有点差,因为
+ UpdatePayment(ctx context.Context, pmt domain.Payment) error
+ FindExpiredPayment(ctx context.Context, offset int, limit int, t time.Time) ([]domain.Payment, error)
+ GetPayment(ctx context.Context, bizTradeNO string) (domain.Payment, error)
+}
diff --git a/webook/payment/service/wechat/native.go b/webook/payment/service/wechat/native.go
new file mode 100644
index 0000000000000000000000000000000000000000..302200f07e1110f8d55e907a10964cd9bfb1230e
--- /dev/null
+++ b/webook/payment/service/wechat/native.go
@@ -0,0 +1,162 @@
+package wechat
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gitee.com/geekbang/basic-go/webook/payment/events"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/wechatpay-apiv3/wechatpay-go/core"
+ "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
+ "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
+ "time"
+)
+
+var errUnknownTransactionState = errors.New("未知的微信事务状态")
+
+type NativePaymentService struct {
+ svc *native.NativeApiService
+ appID string
+ mchID string
+ notifyURL string
+ repo repository.PaymentRepository
+ l logger.LoggerV1
+
+ // 在微信 native 里面,分别是
+ // SUCCESS:支付成功
+ // REFUND:转入退款
+ // NOTPAY:未支付
+ // CLOSED:已关闭
+ // REVOKED:已撤销(付款码支付)
+ // USERPAYING:用户支付中(付款码支付)
+ // PAYERROR:支付失败(其他原因,如银行返回失败)
+ nativeCBTypeToStatus map[string]domain.PaymentStatus
+ producer events.Producer
+}
+
+func NewNativePaymentService(svc *native.NativeApiService,
+ repo repository.PaymentRepository,
+ l logger.LoggerV1,
+ appid, mchid string) *NativePaymentService {
+ return &NativePaymentService{
+ l: l,
+ repo: repo,
+ svc: svc,
+ appID: appid,
+ mchID: mchid,
+ // 一般来说,这个都是固定的,基本不会变的
+ // 这个从配置文件里面读取
+ // 1. 测试环境 test.wechat.meoying.com
+ // 2. 开发环境 dev.wecaht.meoying.com
+ // 3. 线上环境 wechat.meoying.com
+ // DNS 解析到腾讯云
+ // wechat.tencent_cloud.meoying.com
+ // DNS 解析到阿里云
+ // wechat.ali_cloud.meoying.com
+ notifyURL: "http://wechat.meoying.com/pay/callback",
+ nativeCBTypeToStatus: map[string]domain.PaymentStatus{
+ "SUCCESS": domain.PaymentStatusSuccess,
+ "PAYERROR": domain.PaymentStatusFailed,
+ // 这个状态,有些人会考虑映射过去 PaymentStatusFailed
+ "NOTPAY": domain.PaymentStatusInit,
+ "USERPAYING": domain.PaymentStatusInit,
+ "CLOSED": domain.PaymentStatusFailed,
+ "REVOKED": domain.PaymentStatusFailed,
+ "REFUND": domain.PaymentStatusRefund,
+ // 其它状态你都可以加
+ },
+ }
+}
+
+// Prepay 为了拿到扫码支付的二维码
+func (n *NativePaymentService) Prepay(ctx context.Context, pmt domain.Payment) (string, error) {
+ // 唯一索引冲突
+ // 业务方唤起了支付,但是没付,下一次再过来,应该换 BizTradeNO
+ err := n.repo.AddPayment(ctx, pmt)
+ if err != nil {
+ return "", err
+ }
+ //sn := uuid.New().String()
+ resp, result, err := n.svc.Prepay(ctx, native.PrepayRequest{
+ Appid: core.String(n.appID),
+ Mchid: core.String(n.mchID),
+ Description: core.String(pmt.Description),
+ // 这个地方是有讲究的
+ // 选择1:业务方直接给我,我透传,我啥也不干
+ // 选择2:业务方给我它的业务标识,我自己生成一个 - 担忧出现重复
+ // 注意,不管你是选择 1 还是选择 2,业务方都一定要传给你(webook payment)一个唯一标识
+ // Biz + BizTradeNo 唯一, biz + biz_id
+ OutTradeNo: core.String(pmt.BizTradeNO),
+ NotifyUrl: core.String(n.notifyURL),
+ // 设置三十分钟有效
+ TimeExpire: core.Time(time.Now().Add(time.Minute * 30)),
+ Amount: &native.Amount{
+ Total: core.Int64(pmt.Amt.Total),
+ Currency: core.String(pmt.Amt.Currency),
+ },
+ })
+ n.l.Debug("微信prepay响应",
+ logger.Field{Key: "result", Value: result},
+ logger.Field{Key: "resp", Value: resp})
+ if err != nil {
+ return "", err
+ }
+ return *resp.CodeUrl, nil
+}
+
+// SyncWechatInfo 我的兜底,就是我准备同步一下状态
+func (n *NativePaymentService) SyncWechatInfo(ctx context.Context,
+ bizTradeNO string) error {
+ txn, _, err := n.svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
+ OutTradeNo: core.String(bizTradeNO),
+ Mchid: core.String(n.mchID),
+ })
+ if err != nil {
+ return err
+ }
+ return n.updateByTxn(ctx, txn)
+}
+
+func (n *NativePaymentService) FindExpiredPayment(ctx context.Context, offset, limit int, t time.Time) ([]domain.Payment, error) {
+ return n.repo.FindExpiredPayment(ctx, offset, limit, t)
+}
+
+func (n *NativePaymentService) GetPayment(ctx context.Context, bizTradeId string) (domain.Payment, error) {
+ // 在这里,我能不能设计一个慢路径?如果要是不知道支付结果,我就去微信里面查一下?
+ // 或者异步查一下?
+ return n.repo.GetPayment(ctx, bizTradeId)
+}
+
+func (n *NativePaymentService) HandleCallback(ctx context.Context, txn *payments.Transaction) error {
+ return n.updateByTxn(ctx, txn)
+}
+
+func (n *NativePaymentService) updateByTxn(ctx context.Context, txn *payments.Transaction) error {
+ // 搞一个 status 映射的 map
+ status, ok := n.nativeCBTypeToStatus[*txn.TradeState]
+ if !ok {
+ // 这个地方,要告警
+ return errors.New("状态映射失败,未知状态的回调")
+ }
+ // 核心就是更新数据库状态
+ err := n.repo.UpdatePayment(ctx, domain.Payment{
+ BizTradeNO: *txn.OutTradeNo,
+ Status: status,
+ TxnID: *txn.TransactionId,
+ })
+ if err != nil {
+ return err
+ }
+
+ // 发送消息,有结果了总要通知业务方
+ // 这里有很多问题,核心就是部分失败问题,其次还有重复发送问题
+ err1 := n.producer.ProducePaymentEvent(ctx, events.PaymentEvent{
+ BizTradeNO: *txn.OutTradeNo,
+ Status: status.AsUint8(),
+ })
+ if err1 != nil {
+ // 加监控加告警,立刻手动修复,或者自动补发
+ }
+ return nil
+}
diff --git a/webook/payment/service/wechat/native_test.go b/webook/payment/service/wechat/native_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ccb29e4e88b2dd20d0cc85f17495d5ddbb3d5918
--- /dev/null
+++ b/webook/payment/service/wechat/native_test.go
@@ -0,0 +1,60 @@
+package wechat
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/wechatpay-apiv3/wechatpay-go/core"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/option"
+ "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
+ "github.com/wechatpay-apiv3/wechatpay-go/utils"
+ "net/http"
+ "os"
+ "testing"
+)
+
+// 下单
+func TestNativeService_Prepay(t *testing.T) {
+ appid := os.Getenv("WEPAY_APP_ID")
+ mchID := os.Getenv("WEPAY_MCH_ID")
+ mchKey := os.Getenv("WEPAY_MCH_KEY")
+ mchSerialNumber := os.Getenv("WEPAY_MCH_SERIAL_NUM")
+ // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
+ mchPrivateKey, err := utils.LoadPrivateKeyWithPath(
+ "/Users/mindeng/workspace/go/src/geekbang/basic-go/webook/payment/config/cert/apiclient_key.pem",
+ )
+ require.NoError(t, err)
+ ctx := context.Background()
+ // 使用商户私钥等初始化 client
+ client, err := core.NewClient(
+ ctx,
+ option.WithWechatPayAutoAuthCipher(mchID, mchSerialNumber, mchPrivateKey, mchKey),
+ )
+ require.NoError(t, err)
+ nativeSvc := &native.NativeApiService{
+ Client: client,
+ }
+ svc := NewNativePaymentService(nativeSvc, nil, logger.NewNoOpLogger(), appid, mchID)
+ codeUrl, err := svc.Prepay(ctx, domain.Payment{
+ Amt: domain.Amount{
+ Currency: "CNY",
+ Total: 1,
+ },
+ BizTradeNO: "test_123",
+ Description: "面试官AI",
+ })
+ require.NoError(t, err)
+ assert.NotEmpty(t, codeUrl)
+ t.Log(codeUrl)
+}
+
+func TestServer(t *testing.T) {
+ http.HandleFunc("/", func(
+ writer http.ResponseWriter,
+ request *http.Request) {
+ writer.Write([]byte("hello, 我进来了"))
+ })
+ http.ListenAndServe(":8080", nil)
+}
diff --git a/webook/payment/service/wechat/types.go b/webook/payment/service/wechat/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..87a981644b726e74549c6989813967d76301a794
--- /dev/null
+++ b/webook/payment/service/wechat/types.go
@@ -0,0 +1,11 @@
+package wechat
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/payment/domain"
+)
+
+type PaymentService interface {
+ // Prepay 预支付,对应于微信创建订单的步骤
+ Prepay(ctx context.Context, pmt domain.Payment) (string, error)
+}
diff --git a/webook/payment/web/wechat.go b/webook/payment/web/wechat.go
new file mode 100644
index 0000000000000000000000000000000000000000..4a9e4bbc77b003972d5d341853ba9d9ab4ba684c
--- /dev/null
+++ b/webook/payment/web/wechat.go
@@ -0,0 +1,60 @@
+package web
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/service/wechat"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/gin-gonic/gin"
+ "github.com/wechatpay-apiv3/wechatpay-go/core/notify"
+ "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
+ "net/http"
+)
+
+type WechatHandler struct {
+ handler *notify.Handler
+ l logger.LoggerV1
+ nativeSvc *wechat.NativePaymentService
+}
+
+func NewWechatHandler(handler *notify.Handler,
+ nativeSvc *wechat.NativePaymentService,
+ l logger.LoggerV1) *WechatHandler {
+ return &WechatHandler{
+ handler: handler,
+ nativeSvc: nativeSvc,
+ l: l}
+}
+
+func (h *WechatHandler) RegisterRoutes(server *gin.Engine) {
+ server.GET("/hello", func(context *gin.Context) {
+ context.String(http.StatusOK, "我进来了")
+ })
+ // 这地方不能 wrap
+ server.Any("/pay/callback", ginx.Wrap(h.HandleNative))
+}
+
+func (h *WechatHandler) HandleNative(ctx *gin.Context) (ginx.Result, error) {
+ txn := new(payments.Transaction)
+ _, err := h.handler.ParseNotifyRequest(ctx.Request.Context(), ctx.Request, txn)
+ if err != nil {
+
+ // 这里不可能触发对账,你解密都出错了,你拿不到 BizTradeNO
+
+ // 返回非 2xx 的响应
+ // 就一个原因:有人伪造请求,有人在伪造微信支付的回调
+ // 做好监控和告警
+ // 大量进来这个分支,就说明有人搞你
+ return ginx.Result{}, err
+ }
+ // 当你下来这里的时候,交易信息已经被解密好了,放到了 txn 里面
+ // 也就是说,我们现在就是要处理一下 txn 就可以
+ err = h.nativeSvc.HandleCallback(ctx, txn)
+ if err != nil {
+ // 这里处理失败了,你可以再次触发对账
+ // 返回非 2xx 的响应
+ return ginx.Result{}, err
+ }
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
diff --git a/webook/payment/web/wechat_test.go b/webook/payment/web/wechat_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..76bb10c54fd00baf0930763b4142cb545607362c
--- /dev/null
+++ b/webook/payment/web/wechat_test.go
@@ -0,0 +1,24 @@
+package web
+
+import (
+ "bytes"
+ "github.com/stretchr/testify/require"
+ "io"
+ "net/http"
+ "testing"
+)
+
+// 为了验证处理回调的逻辑,最好是手动记下来一个请求
+// 这样可以反复运行测试
+func TestHandleCallback(t *testing.T) {
+ body := `
+{"id":"13de4f2b-a78b-5e2f-8773-b30f39b0d341","create_time":"2024-01-11T21:16:33+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"UxIDjTS1LqTUqy/6KpsCuqYf+FeBInzOYgjeGsomF6PUmUVjW5XlD0PUSvqZqZtLkbNHJkRLd/HDJYHItpkRy3l8xhx3SGd3B2tvWO0un5w7BBpUo8XetEiXoVagzSpvUDrIKBczckM6ILZRK766dHNx7Z82d10sBAls4eCQwg446gojdU3uOPzM6TxGw8Muf3NJsNh+B0F2tZmwDk0yi4NTgaOwZIPv8Q1l/LmAhbBVLOJCNz28XKGMckQWGyzkTX0X9wiNPTJ3GrEwwPcnwTvERqvFhWsfHK6iewCuAlFs6D4Ta8pi6cs2i2ehBeMgxgNCOYc0LFpIGyd38G3i8+1wCzWFKz3PfswUbNU6w+8elhv8l6YYnnBc/pqq20lwnDpx/DOhTBqMn16TU+cD2rA2UlpMG5NdK+GpysJzZtbCqEg2lgL8xMsHlP4JnIWH6HjVXHMY5tOSN3v6NCuo88/IW6w0UDPbPG4fx5xD8RjTpwEO0nMnHO2JOm2sKfnlQ5pocaGhYUfpvW3AfP54f2bF335l20LctQYnc0B519Lnpqeo6phAgCKdtLl/h2/OuPK/","associated_data":"transaction","nonce":"CkVASomQkPSt"}}
+`
+ req, err := http.NewRequest(http.MethodPost, "/pay/callback", io.NopCloser(bytes.NewBufferString(body)))
+ require.NoError(t, err)
+ req.Header.Add("Wechatpay-Serial", "4522CC613021F88AECCFAA187F24BB9A8C73ED61")
+ req.Header.Add("Wechatpay-Signature", "pWPMNLXxrIcPTg9R8Tsw9D4RaEb4MBt1eLTaLejXtLctqmAxFYuxABLrR+710HNwMtuupupfok8sixlZMfEcHIdsq14ZTGViEB/5j4bMrBJCAid6Dv/zGuJ6rGlOptAb/+ebLDoshGlI1BL3SnUVBFpOnKmj88Ak4SCnZ9iMq9/HzimxvLkOHWgau+WooKkurRd1r41r1W0wmM0ICgetHwwCMe1jA2D2stCHFZR577+PomDBLT1zUDSAXfKWNetnnukNZqnG4Y4huMd9m6OtfnSfl96UJxhghFn1RDMpBgsDdz6VHuxIAOCmPHYFbCyXiRUYjpP8AhROHc3mxdZEnw==")
+ req.Header.Add("Wechatpay-Signature-Type", "WECHATPAY2-SHA256-RSA2048")
+ req.Header.Add("Wechatpay-Timestamp", "1704979024")
+ req.Header.Add("Wechatpay-Nonce", "uhnrNaJIvLnRlDJaYNGA2Q4rcymFBkXE")
+}
diff --git a/webook/payment/wire.go b/webook/payment/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..7872443b5d529a104308b06fe282b230f6789b91
--- /dev/null
+++ b/webook/payment/wire.go
@@ -0,0 +1,34 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/grpc"
+ "gitee.com/geekbang/basic-go/webook/payment/ioc"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/payment/web"
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+ "github.com/google/wire"
+)
+
+func InitApp() *wego.App {
+ wire.Build(
+ ioc.InitEtcdClient,
+ ioc.InitKafka,
+ ioc.InitProducer,
+ ioc.InitWechatClient,
+ dao.NewPaymentGORMDAO,
+ ioc.InitDB,
+ repository.NewPaymentRepository,
+ grpc.NewWechatServiceServer,
+ ioc.InitWechatNativeService,
+ ioc.InitWechatConfig,
+ ioc.InitWechatNotifyHandler,
+ ioc.InitGRPCServer,
+ web.NewWechatHandler,
+ ioc.InitGinServer,
+ ioc.InitLogger,
+ wire.Struct(new(wego.App), "WebServer", "GRPCServer"))
+ return new(wego.App)
+}
diff --git a/webook/payment/wire_gen.go b/webook/payment/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1baf11d4e4993b1721e4e6c5c5127c252fc5e19
--- /dev/null
+++ b/webook/payment/wire_gen.go
@@ -0,0 +1,41 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/payment/grpc"
+ "gitee.com/geekbang/basic-go/webook/payment/ioc"
+ "gitee.com/geekbang/basic-go/webook/payment/repository"
+ "gitee.com/geekbang/basic-go/webook/payment/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/payment/web"
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+)
+
+// Injectors from wire.go:
+
+func InitApp() *wego.App {
+ wechatConfig := ioc.InitWechatConfig()
+ handler := ioc.InitWechatNotifyHandler(wechatConfig)
+ client := ioc.InitWechatClient(wechatConfig)
+ db := ioc.InitDB()
+ paymentDAO := dao.NewPaymentGORMDAO(db)
+ paymentRepository := repository.NewPaymentRepository(paymentDAO)
+ loggerV1 := ioc.InitLogger()
+ saramaClient := ioc.InitKafka()
+ producer := ioc.InitProducer(saramaClient)
+ nativePaymentService := ioc.InitWechatNativeService(client, paymentRepository, loggerV1, producer, wechatConfig)
+ wechatHandler := web.NewWechatHandler(handler, nativePaymentService, loggerV1)
+ server := ioc.InitGinServer(wechatHandler)
+ wechatServiceServer := grpc.NewWechatServiceServer(nativePaymentService)
+ clientv3Client := ioc.InitEtcdClient()
+ grpcxServer := ioc.InitGRPCServer(wechatServiceServer, clientv3Client, loggerV1)
+ app := &wego.App{
+ WebServer: server,
+ GRPCServer: grpcxServer,
+ }
+ return app
+}
diff --git a/webook/pkg/canalx/kafka.go b/webook/pkg/canalx/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..01d2b806092696c48d595e4a0fd8ac1fd999daaf
--- /dev/null
+++ b/webook/pkg/canalx/kafka.go
@@ -0,0 +1,57 @@
+package canalx
+
+// Message 可以根据需要把其它字段也加入进来。
+// T 直接对应到表结构
+type Message[T any] struct {
+ Data []T `json:"data"`
+ Database string `json:"database"`
+ Table string `json:"table"`
+ Type string `json:"type"`
+}
+
+type MessageV1 struct {
+ Data []struct {
+ Id string `json:"id"`
+ Uid string `json:"uid"`
+ Biz string `json:"biz"`
+ BizId string `json:"biz_id"`
+ RootId interface{} `json:"root_id"`
+ Pid interface{} `json:"pid"`
+ Content string `json:"content"`
+ Ctime string `json:"ctime"`
+ Utime string `json:"utime"`
+ } `json:"data"`
+ Database string `json:"database"`
+ Es int64 `json:"es"`
+ Gtid string `json:"gtid"`
+ Id int `json:"id"`
+ IsDdl bool `json:"isDdl"`
+ MysqlType struct {
+ Id string `json:"id"`
+ Uid string `json:"uid"`
+ Biz string `json:"biz"`
+ BizId string `json:"biz_id"`
+ RootId string `json:"root_id"`
+ Pid string `json:"pid"`
+ Content string `json:"content"`
+ Ctime string `json:"ctime"`
+ Utime string `json:"utime"`
+ } `json:"mysqlType"`
+ Old interface{} `json:"old"`
+ PkNames []string `json:"pkNames"`
+ Sql string `json:"sql"`
+ SqlType struct {
+ Id int `json:"id"`
+ Uid int `json:"uid"`
+ Biz int `json:"biz"`
+ BizId int `json:"biz_id"`
+ RootId int `json:"root_id"`
+ Pid int `json:"pid"`
+ Content int `json:"content"`
+ Ctime int `json:"ctime"`
+ Utime int `json:"utime"`
+ } `json:"sqlType"`
+ Table string `json:"table"`
+ Ts int64 `json:"ts"`
+ Type string `json:"type"`
+}
diff --git a/webook/pkg/ginx/handdle_func.go b/webook/pkg/ginx/handdle_func.go
new file mode 100644
index 0000000000000000000000000000000000000000..df3e31346c048570e13c9a65cd3ecb34e9f71c96
--- /dev/null
+++ b/webook/pkg/ginx/handdle_func.go
@@ -0,0 +1,19 @@
+package ginx
+
+import (
+ "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "github.com/gin-gonic/gin"
+)
+
+func WrapReq[T any](fn func(ctx *gin.Context, req T, uc jwt.UserClaims) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ // 顺便把 userClaims 也取出来
+ }
+}
+
+type Result struct {
+ // 这个叫做业务错误码
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data any `json:"data"`
+}
diff --git a/webook/pkg/ginx/middlewares/logger/builder.go b/webook/pkg/ginx/middlewares/logger/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..2c3e5876248c9a8c0d803fc492e7887df6cab4e9
--- /dev/null
+++ b/webook/pkg/ginx/middlewares/logger/builder.go
@@ -0,0 +1,119 @@
+package logger
+
+import (
+ "bytes"
+ "context"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/atomic"
+ "io"
+ "time"
+)
+
+// MiddlewareBuilder 注意点:
+// 1. 小心日志内容过多。URL 可能很长,请求体,响应体都可能很大,你要考虑是不是完全输出到日志里面
+// 2. 考虑 1 的问题,以及用户可能换用不同的日志框架,所以要有足够的灵活性
+// 3. 考虑动态开关,结合监听配置文件,要小心并发安全
+type MiddlewareBuilder struct {
+ allowReqBody *atomic.Bool
+ allowRespBody bool
+ loggerFunc func(ctx context.Context, al *AccessLog)
+}
+
+func NewBuilder(fn func(ctx context.Context, al *AccessLog)) *MiddlewareBuilder {
+ return &MiddlewareBuilder{
+ loggerFunc: fn,
+ allowReqBody: atomic.NewBool(false),
+ }
+}
+
+func (b *MiddlewareBuilder) AllowReqBody(ok bool) *MiddlewareBuilder {
+ b.allowReqBody.Store(ok)
+ return b
+}
+
+func (b *MiddlewareBuilder) AllowRespBody() *MiddlewareBuilder {
+ b.allowRespBody = true
+ return b
+}
+
+func (b *MiddlewareBuilder) Build() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ // 借助 http header 来传递超时信息
+ //timeout := ctx.GetHeader("x-timeout").(int64)
+ //t := time.UnixMilli(timeout)
+ //reqCtx, cancel := context.WithDeadline(ctx, t)
+ start := time.Now()
+ url := ctx.Request.URL.String()
+ if len(url) > 1024 {
+ url = url[:1024]
+ }
+ al := &AccessLog{
+ Method: ctx.Request.Method,
+ // URL 本身也可能很长
+ Url: url,
+ }
+ if b.allowReqBody.Load() && ctx.Request.Body != nil {
+ // Body 读完就没有了
+ body, _ := ctx.GetRawData()
+ reader := io.NopCloser(bytes.NewReader(body))
+ ctx.Request.Body = reader
+ //ctx.Request.GetBody = func() (io.ReadCloser, error) {
+ // return reader, nil
+ //}
+
+ if len(body) > 1024 {
+ body = body[:1024]
+ }
+ // 这其实是一个很消耗 CPU 和内存的操作
+ // 因为会引起复制
+ al.ReqBody = string(body)
+ }
+
+ if b.allowRespBody {
+ ctx.Writer = responseWriter{
+ al: al,
+ ResponseWriter: ctx.Writer,
+ }
+ }
+
+ defer func() {
+ al.Duration = time.Since(start).String()
+ b.loggerFunc(ctx, al)
+ }()
+
+ // 执行到业务逻辑
+ ctx.Next()
+
+ //b.loggerFunc(ctx, al)
+ }
+}
+
+type responseWriter struct {
+ al *AccessLog
+ gin.ResponseWriter
+}
+
+func (w responseWriter) WriteHeader(statusCode int) {
+ w.al.Status = statusCode
+ w.ResponseWriter.WriteHeader(statusCode)
+}
+func (w responseWriter) Write(data []byte) (int, error) {
+ w.al.RespBody = string(data)
+ return w.ResponseWriter.Write(data)
+}
+
+func (w responseWriter) WriteString(data string) (int, error) {
+ w.al.RespBody = data
+ return w.ResponseWriter.WriteString(data)
+}
+
+type AccessLog struct {
+ // HTTP 请求的方法
+ Method string
+ // Url 整个请求 URL
+ Url string
+ Duration string
+ ReqBody string
+ RespBody string
+ Status int
+}
diff --git a/webook/pkg/ginx/middlewares/metric/prometheus.go b/webook/pkg/ginx/middlewares/metric/prometheus.go
new file mode 100644
index 0000000000000000000000000000000000000000..d6f6fec3e9ccc6f88f8475993e386379beb0657e
--- /dev/null
+++ b/webook/pkg/ginx/middlewares/metric/prometheus.go
@@ -0,0 +1,69 @@
+package metric
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus"
+ "strconv"
+ "time"
+)
+
+type MiddlewareBuilder struct {
+ Namespace string
+ Subsystem string
+ Name string
+ Help string
+ InstanceID string
+}
+
+func (m *MiddlewareBuilder) Build() gin.HandlerFunc {
+ // pattern 是指你命中的路由
+ // 是指你的 HTTP 的 status
+ // path /detail/1
+ labels := []string{"method", "pattern", "status"}
+ summary := prometheus.NewSummaryVec(prometheus.SummaryOpts{
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Name: m.Name + "_resp_time",
+ Help: m.Help,
+ ConstLabels: map[string]string{
+ "instance_id": m.InstanceID,
+ },
+ Objectives: map[float64]float64{
+ 0.5: 0.01,
+ 0.9: 0.01,
+ 0.99: 0.005,
+ 0.999: 0.0001,
+ },
+ }, labels)
+ prometheus.MustRegister(summary)
+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Name: m.Name + "_active_req",
+ Help: m.Help,
+ ConstLabels: map[string]string{
+ "instance_id": m.InstanceID,
+ },
+ })
+ prometheus.MustRegister(gauge)
+ return func(ctx *gin.Context) {
+ start := time.Now()
+ gauge.Inc()
+ defer func() {
+ duration := time.Since(start)
+ gauge.Dec()
+ // 404????
+ pattern := ctx.FullPath()
+ if pattern == "" {
+ pattern = "unknown"
+ }
+ summary.WithLabelValues(
+ ctx.Request.Method,
+ pattern,
+ strconv.Itoa(ctx.Writer.Status()),
+ ).Observe(float64(duration.Milliseconds()))
+ }()
+ // 你最终就会执行到业务里面
+ ctx.Next()
+ }
+}
diff --git a/webook/pkg/ginx/middlewares/ratelimit/builder.go b/webook/pkg/ginx/middlewares/ratelimit/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..98d88ea6bbff95c878c4edfffaeeed04462da12a
--- /dev/null
+++ b/webook/pkg/ginx/middlewares/ratelimit/builder.go
@@ -0,0 +1,51 @@
+package ratelimit
+
+import (
+ _ "embed"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/pkg/ratelimit"
+ "github.com/gin-gonic/gin"
+ "log"
+ "net/http"
+)
+
+type Builder struct {
+ prefix string
+ limiter ratelimit.Limiter
+}
+
+func NewBuilder(limiter ratelimit.Limiter) *Builder {
+ return &Builder{
+ prefix: "ip-limiter",
+ limiter: limiter,
+ }
+}
+
+func (b *Builder) Prefix(prefix string) *Builder {
+ b.prefix = prefix
+ return b
+}
+
+func (b *Builder) Build() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ limited, err := b.limit(ctx)
+ if err != nil {
+ log.Println(err)
+ // 这一步很有意思,就是如果这边出错了
+ // 要怎么办?
+ ctx.AbortWithStatus(http.StatusInternalServerError)
+ return
+ }
+ if limited {
+ log.Println(err)
+ ctx.AbortWithStatus(http.StatusTooManyRequests)
+ return
+ }
+ ctx.Next()
+ }
+}
+
+func (b *Builder) limit(ctx *gin.Context) (bool, error) {
+ key := fmt.Sprintf("%s:%s", b.prefix, ctx.ClientIP())
+ return b.limiter.Limit(ctx, key)
+}
diff --git a/webook/pkg/ginx/server.go b/webook/pkg/ginx/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..ae225f4596903e01bdad678426a3327df77c86d6
--- /dev/null
+++ b/webook/pkg/ginx/server.go
@@ -0,0 +1,12 @@
+package ginx
+
+import "github.com/gin-gonic/gin"
+
+type Server struct {
+ *gin.Engine
+ Addr string
+}
+
+func (s *Server) Start() error {
+ return s.Engine.Run(s.Addr)
+}
diff --git a/webook/pkg/ginx/wrappper.go b/webook/pkg/ginx/wrappper.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f250ff7ce73f737cd3736655245f5e7e7df7d1a
--- /dev/null
+++ b/webook/pkg/ginx/wrappper.go
@@ -0,0 +1,155 @@
+package ginx
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/prometheus/client_golang/prometheus"
+ "net/http"
+ "strconv"
+)
+
+// 这个东西,放到你们的 ginx 插件库里面去
+// 技术含量不是很高,但是绝对有技巧
+
+// L 使用包变量
+var L logger.LoggerV1
+
+var vector *prometheus.CounterVec
+
+func InitCounter(opt prometheus.CounterOpts) {
+ vector = prometheus.NewCounterVec(opt,
+ []string{"code"})
+ prometheus.MustRegister(vector)
+ // 你可以考虑使用 code, method, 命中路由,HTTP 状态码
+}
+
+func Wrap(fn func(ctx *gin.Context) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ res, err := fn(ctx)
+ if err != nil {
+ // 开始处理 error,其实就是记录一下日志
+ L.Error("处理业务逻辑出错",
+ logger.String("path", ctx.Request.URL.Path),
+ // 命中的路由
+ logger.String("route", ctx.FullPath()),
+ logger.Error(err))
+ }
+ vector.WithLabelValues(strconv.Itoa(res.Code)).Inc()
+ ctx.JSON(http.StatusOK, res)
+ }
+}
+
+func WrapToken[C jwt.Claims](fn func(ctx *gin.Context, uc C) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ // 执行一些东西
+ val, ok := ctx.Get("users")
+ if !ok {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ c, ok := val.(C)
+ if !ok {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ // 下半段的业务逻辑从哪里来?
+ // 我的业务逻辑有可能要操作 ctx
+ // 你要读取 HTTP HEADER
+ res, err := fn(ctx, c)
+ if err != nil {
+ // 开始处理 error,其实就是记录一下日志
+ L.Error("处理业务逻辑出错",
+ logger.String("path", ctx.Request.URL.Path),
+ // 命中的路由
+ logger.String("route", ctx.FullPath()),
+ logger.Error(err))
+ }
+ vector.WithLabelValues(strconv.Itoa(res.Code)).Inc()
+ ctx.JSON(http.StatusOK, res)
+ // 再执行一些东西
+ }
+}
+
+func WrapBodyAndToken[Req any, C jwt.Claims](fn func(ctx *gin.Context, req Req, uc C) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var req Req
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+
+ val, ok := ctx.Get("users")
+ if !ok {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ c, ok := val.(C)
+ if !ok {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ // 下半段的业务逻辑从哪里来?
+ // 我的业务逻辑有可能要操作 ctx
+ // 你要读取 HTTP HEADER
+ res, err := fn(ctx, req, c)
+ if err != nil {
+ // 开始处理 error,其实就是记录一下日志
+ L.Error("处理业务逻辑出错",
+ logger.String("path", ctx.Request.URL.Path),
+ // 命中的路由
+ logger.String("route", ctx.FullPath()),
+ logger.Error(err))
+ }
+ ctx.JSON(http.StatusOK, res)
+ }
+}
+
+func WrapBodyV1[T any](fn func(ctx *gin.Context, req T) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var req T
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+
+ // 下半段的业务逻辑从哪里来?
+ // 我的业务逻辑有可能要操作 ctx
+ // 你要读取 HTTP HEADER
+ res, err := fn(ctx, req)
+ if err != nil {
+ // 开始处理 error,其实就是记录一下日志
+ L.Error("处理业务逻辑出错",
+ logger.String("path", ctx.Request.URL.Path),
+ // 命中的路由
+ logger.String("route", ctx.FullPath()),
+ logger.Error(err))
+ }
+ ctx.JSON(http.StatusOK, res)
+ }
+}
+
+func WrapBody[T any](l logger.LoggerV1, fn func(ctx *gin.Context, req T) (Result, error)) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var req T
+ if err := ctx.Bind(&req); err != nil {
+ return
+ }
+
+ // 下半段的业务逻辑从哪里来?
+ // 我的业务逻辑有可能要操作 ctx
+ // 你要读取 HTTP HEADER
+ res, err := fn(ctx, req)
+ if err != nil {
+ // 开始处理 error,其实就是记录一下日志
+ l.Error("处理业务逻辑出错",
+ logger.String("path", ctx.Request.URL.Path),
+ // 命中的路由
+ logger.String("route", ctx.FullPath()),
+ logger.Error(err))
+ }
+ ctx.JSON(http.StatusOK, res)
+ }
+}
diff --git a/webook/pkg/gormx/connpool/double_write_pool.go b/webook/pkg/gormx/connpool/double_write_pool.go
new file mode 100644
index 0000000000000000000000000000000000000000..19b3da57c1c87aad345dabf153e68624092fe085
--- /dev/null
+++ b/webook/pkg/gormx/connpool/double_write_pool.go
@@ -0,0 +1,327 @@
+package connpool
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "gorm.io/gorm"
+)
+
+var errUnknownPattern = errors.New("未知的双写模式")
+
+type DoubleWritePool struct {
+ src gorm.ConnPool
+ dst gorm.ConnPool
+ pattern *atomicx.Value[string]
+}
+
+func NewDoubleWritePool(src gorm.ConnPool,
+ dst gorm.ConnPool, pattern string) *DoubleWritePool {
+ return &DoubleWritePool{src: src, dst: dst, pattern: atomicx.NewValueOf(pattern)}
+}
+
+func (d *DoubleWritePool) BeginTx(ctx context.Context, opts *sql.TxOptions) (gorm.ConnPool, error) {
+ pattern := d.pattern.Load()
+ switch pattern {
+ case PatternSrcOnly:
+ tx, err := d.src.(gorm.TxBeginner).BeginTx(ctx, opts)
+ return &DoubleWritePoolTx{
+ src: tx,
+ pattern: pattern,
+ }, err
+ case PatternSrcFirst:
+ srcTx, err := d.src.(gorm.TxBeginner).BeginTx(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ dstTx, err := d.dst.(gorm.TxBeginner).BeginTx(ctx, opts)
+ if err != nil {
+ // 记录日志,然后不做处理
+
+ // 可以考虑回滚
+ // err = srcTx.Rollback()
+ // return err
+ }
+ return &DoubleWritePoolTx{
+ src: srcTx,
+ dst: dstTx,
+ pattern: pattern,
+ }, nil
+
+ case PatternDstOnly:
+ tx, err := d.dst.(gorm.TxBeginner).BeginTx(ctx, opts)
+ return &DoubleWritePoolTx{
+ src: tx,
+ pattern: pattern,
+ }, err
+ case PatternDstFirst:
+ dstTx, err := d.dst.(gorm.TxBeginner).BeginTx(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ srcTx, err := d.src.(gorm.TxBeginner).BeginTx(ctx, opts)
+ if err != nil {
+ // 记录日志,然后不做处理
+
+ // 可以考虑回滚
+ // err = dstTx.Rollback()
+ // return err
+ }
+ return &DoubleWritePoolTx{
+ src: srcTx,
+ dst: dstTx,
+ pattern: pattern,
+ }, nil
+ default:
+ return nil, errors.New("未知的双写模式")
+ }
+}
+
+// PrepareContext Prepare 的语句会进来这里
+func (d *DoubleWritePool) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
+ // sql.Stmt 是一个结构体,你没有办法说返回一个代表双写的 Stmt
+ panic("implement me")
+ //return nil, errors.New("双写模式下不支持")
+ //switch d.pattern.Load() {
+ //case PatternSrcOnly, PatternSrcFirst:
+ // return d.src.PrepareContext(ctx, query)
+ //case PatternDstOnly, PatternDstFirst:
+ // return d.dst.PrepareContext(ctx, query)
+ //default:
+ // panic("未知的双写模式")
+ // //return nil, errors.New("未知的双写模式")
+ //}
+}
+
+// 在增量校验的时候,我能不能利用这个方法?
+// 1.1 我能不能从 query 里面抽取出来主键, WHERE id= xxx ,然后我就知道哪些数据被影响了?
+// 1.2 可以尝试的思路是:用抽象语法树来分析 query, 而后找出 query 里面的条件,执行一个 SELECT,判定有哪些 id
+// 1.2.1 UPDATE xx set b = xx WHERE a = 1
+// 1.2.2 UPDATE xx set a = xx WHERE a = 1 LIMIT 10; DELETE from xxx WHERE aa OFFSET abc LIMIT cde
+// 1.2.3 INSERT INTO ON CONFLICT, upsert 语句
+func (d *DoubleWritePool) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+ switch d.pattern.Load() {
+ case PatternSrcOnly:
+ return d.src.ExecContext(ctx, query, args...)
+ case PatternSrcFirst:
+ res, err := d.src.ExecContext(ctx, query, args...)
+ if err != nil {
+ return res, err
+ }
+ _, err = d.dst.ExecContext(ctx, query, args...)
+ if err != nil {
+ // 记日志
+ // dst 写失败,不被认为是失败
+ }
+ return res, err
+ case PatternDstOnly:
+ return d.dst.ExecContext(ctx, query, args...)
+ case PatternDstFirst:
+ res, err := d.dst.ExecContext(ctx, query, args...)
+ if err != nil {
+ return res, err
+ }
+ _, err = d.src.ExecContext(ctx, query, args...)
+ if err != nil {
+ // 记日志
+ // dst 写失败,不被认为是失败
+ }
+ return res, err
+ default:
+ panic("未知的双写模式")
+ //return nil, errors.New("未知的双写模式")
+ }
+}
+
+func (d *DoubleWritePool) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
+ switch d.pattern.Load() {
+ case PatternSrcOnly, PatternSrcFirst:
+ return d.src.QueryContext(ctx, query, args...)
+ case PatternDstOnly, PatternDstFirst:
+ return d.dst.QueryContext(ctx, query, args...)
+ default:
+ panic("未知的双写模式")
+ //return nil, errors.New("未知的双写模式")
+ }
+}
+
+func (d *DoubleWritePool) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
+ switch d.pattern.Load() {
+ case PatternSrcOnly, PatternSrcFirst:
+ return d.src.QueryRowContext(ctx, query, args...)
+ case PatternDstOnly, PatternDstFirst:
+ return d.dst.QueryRowContext(ctx, query, args...)
+ default:
+ // 这里有一个问题,我怎么返回一个 error
+ // unsafe 可以
+ panic("未知的双写模式")
+ }
+}
+
+func (d *DoubleWritePool) UpdatePattern(pattern string) {
+ d.pattern.Store(pattern)
+ // 我能不能,有事务未提交的情况下,我禁止修改
+ // 能,但是性能问题比较严重,你需要维持住一个已开事务的计数,要用锁了
+}
+
+type DoubleWritePoolTx struct {
+ src *sql.Tx
+ dst *sql.Tx
+ pattern string
+}
+
+// Commit 和 PPT 不一致
+func (d *DoubleWritePoolTx) Commit() error {
+ switch d.pattern {
+ case PatternSrcOnly:
+ return d.src.Commit()
+ case PatternSrcFirst:
+ // 源库上的事务失败了,我目标库要不要提交
+ // commit 失败了怎么办?
+ err := d.src.Commit()
+ if err != nil {
+ // 要不要提交?
+ return err
+ }
+ if d.dst != nil {
+ err = d.dst.Commit()
+ if err != nil {
+ // 记录日志
+ }
+ }
+ return nil
+ case PatternDstOnly:
+ return d.dst.Commit()
+ case PatternDstFirst:
+ err := d.dst.Commit()
+ if err != nil {
+ // 要不要提交?
+ return err
+ }
+ if d.src != nil {
+ err = d.src.Commit()
+ if err != nil {
+ // 记录日志
+ }
+ }
+ return nil
+ default:
+ return errUnknownPattern
+ }
+}
+
+func (d *DoubleWritePoolTx) Rollback() error {
+ switch d.pattern {
+ case PatternSrcOnly:
+ return d.src.Rollback()
+ case PatternSrcFirst:
+ // 源库上的事务失败了,我目标库要不要提交
+ // commit 失败嘞怎么办?
+ err := d.src.Rollback()
+ if err != nil {
+ // 要不要提交?
+ // 我个人觉得 可以尝试 rollback
+ return err
+ }
+ if d.dst != nil {
+ err = d.dst.Rollback()
+ if err != nil {
+ // 记录日志
+ }
+ }
+ return nil
+ case PatternDstOnly:
+ return d.dst.Rollback()
+ case PatternDstFirst:
+ err := d.dst.Rollback()
+ if err != nil {
+ // 要不要提交?
+ return err
+ }
+ if d.src != nil {
+ err = d.src.Rollback()
+ if err != nil {
+ // 记录日志
+ }
+ }
+ return nil
+ default:
+ return errUnknownPattern
+ }
+}
+
+func (d *DoubleWritePoolTx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
+ panic("implement me")
+}
+
+func (d *DoubleWritePoolTx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+ switch d.pattern {
+ case PatternSrcOnly:
+ return d.src.ExecContext(ctx, query, args...)
+ case PatternSrcFirst:
+ res, err := d.src.ExecContext(ctx, query, args...)
+ if err != nil {
+ return res, err
+ }
+ if d.dst == nil {
+ return res, err
+ }
+ _, err = d.dst.ExecContext(ctx, query, args...)
+ if err != nil {
+ // 记日志
+ // dst 写失败,不被认为是失败
+ }
+ return res, err
+ case PatternDstOnly:
+ return d.dst.ExecContext(ctx, query, args...)
+ case PatternDstFirst:
+ res, err := d.dst.ExecContext(ctx, query, args...)
+ if err != nil {
+ return res, err
+ }
+ if d.src == nil {
+ return res, err
+ }
+ _, err = d.src.ExecContext(ctx, query, args...)
+ if err != nil {
+ // 记日志
+ // dst 写失败,不被认为是失败
+ }
+ return res, err
+ default:
+ panic("未知的双写模式")
+ //return nil, errors.New("未知的双写模式")
+ }
+}
+
+func (d *DoubleWritePoolTx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
+ switch d.pattern {
+ case PatternSrcOnly, PatternSrcFirst:
+ return d.src.QueryContext(ctx, query, args...)
+ case PatternDstOnly, PatternDstFirst:
+ return d.dst.QueryContext(ctx, query, args...)
+ default:
+ panic("未知的双写模式")
+ //return nil, errors.New("未知的双写模式")
+ }
+}
+
+func (d *DoubleWritePoolTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
+ switch d.pattern {
+ case PatternSrcOnly, PatternSrcFirst:
+ return d.src.QueryRowContext(ctx, query, args...)
+ case PatternDstOnly, PatternDstFirst:
+ return d.dst.QueryRowContext(ctx, query, args...)
+ default:
+ panic("未知的双写模式")
+ //return nil, errors.New("未知的双写模式")
+ }
+}
+
+const (
+ PatternDstOnly = "DST_ONLY"
+ PatternSrcOnly = "SRC_ONLY"
+ PatternDstFirst = "DST_FIRST"
+ PatternSrcFirst = "SRC_FIRST"
+)
diff --git a/webook/pkg/gormx/connpool/double_write_pool_test.go b/webook/pkg/gormx/connpool/double_write_pool_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ad1b06ffa4e7066a85b0089dad0bf949f16cd31
--- /dev/null
+++ b/webook/pkg/gormx/connpool/double_write_pool_test.go
@@ -0,0 +1,67 @@
+package connpool
+
+import (
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "github.com/stretchr/testify/require"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "testing"
+)
+
+func TestConnPool(t *testing.T) {
+ webook, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook"))
+ require.NoError(t, err)
+ err = webook.AutoMigrate(&Interactive{})
+ require.NoError(t, err)
+ intr, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook_intr"))
+ require.NoError(t, err)
+ err = intr.AutoMigrate(&Interactive{})
+ require.NoError(t, err)
+ db, err := gorm.Open(mysql.New(mysql.Config{
+ Conn: &DoubleWritePool{
+ src: webook.ConnPool,
+ dst: intr.ConnPool,
+ pattern: atomicx.NewValueOf(PatternSrcFirst),
+ },
+ }))
+ require.NoError(t, err)
+ t.Log(db)
+ err = db.Create(&Interactive{
+ Biz: "test",
+ BizId: 123,
+ }).Error
+ require.NoError(t, err)
+
+ err = db.Transaction(func(tx *gorm.DB) error {
+ err1 := tx.Create(&Interactive{
+ Biz: "test_tx",
+ BizId: 123,
+ }).Error
+
+ return err1
+ })
+
+ require.NoError(t, err)
+
+ err = db.Model(&Interactive{}).Where("id > ?", 0).Updates(map[string]any{
+ "biz_id": 789,
+ }).Error
+ require.NoError(t, err)
+}
+
+type Interactive struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ BizId int64 `gorm:"uniqueIndex:biz_type_id"`
+ Biz string `gorm:"type:varchar(128);uniqueIndex:biz_type_id"`
+ ReadCnt int64
+ CollectCnt int64
+ // 作业:就是直接在 LikeCnt 上创建一个索引
+ // 1. 而后查询前 100 的,直接就命中索引,这样你前 100 最多 100 次回表
+ // SELECT * FROM interactives ORDER BY like_cnt limit 0, 100
+ // 还有一种优化思路是
+ // SELECT * FROM interactives WHERE like_cnt > 1000 ORDER BY like_cnt limit 0, 100
+ // 2. 如果你只需要 biz_id 和 biz_type,你就创建联合索引
+ LikeCnt int64
+ Ctime int64
+ Utime int64
+}
diff --git a/webook/pkg/gormx/connpool/mysql_mongo.go b/webook/pkg/gormx/connpool/mysql_mongo.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab6eb74928ce2b7cb758473243e43ff9ff83c0ce
--- /dev/null
+++ b/webook/pkg/gormx/connpool/mysql_mongo.go
@@ -0,0 +1,25 @@
+package connpool
+
+import (
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "go.mongodb.org/mongo-driver/mongo"
+ "gorm.io/gorm"
+)
+
+type MySQL2Mongo struct {
+ db gorm.ConnPool
+ mdb *mongo.Database
+ pattern *atomicx.Value[string]
+}
+
+//func (d *MySQL2Mongo) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
+// switch d.pattern.Load() {
+// case PatternSrcOnly, PatternSrcFirst:
+// return d.db.QueryContext(ctx, query, args...)
+// case PatternDstOnly, PatternDstFirst:
+// return d.mdb.Collection("xxx").FindOne()
+// default:
+// panic("未知的双写模式")
+// //return nil, errors.New("未知的双写模式")
+// }
+//}
diff --git a/webook/pkg/gormx/connpool/sharding.go b/webook/pkg/gormx/connpool/sharding.go
new file mode 100644
index 0000000000000000000000000000000000000000..8ae1ad7217adea1ab0b1234557d06052e76f30c1
--- /dev/null
+++ b/webook/pkg/gormx/connpool/sharding.go
@@ -0,0 +1,4 @@
+package connpool
+
+type ShardingPool struct {
+}
diff --git a/webook/pkg/gormx/connpool/write_split.go b/webook/pkg/gormx/connpool/write_split.go
new file mode 100644
index 0000000000000000000000000000000000000000..14d9d6579e7d011ccd2c03ba71590430eb20bb3c
--- /dev/null
+++ b/webook/pkg/gormx/connpool/write_split.go
@@ -0,0 +1,39 @@
+package connpool
+
+import (
+ "context"
+ "database/sql"
+ "gorm.io/gorm"
+)
+
+// WriteSplit 主从模式
+type WriteSplit struct {
+ master gorm.ConnPool
+ slaves []gorm.ConnPool
+}
+
+func (w *WriteSplit) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) {
+ return w.master.(gorm.TxBeginner).BeginTx(ctx, opts)
+}
+
+func (w *WriteSplit) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
+ // 可以默认返回 master,也可以默认返回 slave
+ return w.master.PrepareContext(ctx, query)
+}
+
+func (w *WriteSplit) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+ return w.master.ExecContext(ctx, query, args...)
+}
+
+func (w *WriteSplit) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
+ // slaves 要考虑负载均衡, 搞个轮询 slaves
+ // 这边可以玩骚操作,轮询,加权轮询,平滑的加权轮询,随机,加权随机
+ // 动态判定 slaves 健康情况的负载均衡策略
+ //(永远挑选最快返回响应的那个 slave,或者暂时禁用超时的 slaves)
+ panic("implement me")
+}
+
+func (w *WriteSplit) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
+ //TODO implement me
+ panic("implement me")
+}
diff --git a/webook/pkg/gormx/double_write_callback.go b/webook/pkg/gormx/double_write_callback.go
new file mode 100644
index 0000000000000000000000000000000000000000..54ef4598c114e9dc320c34833a730fcadb409f77
--- /dev/null
+++ b/webook/pkg/gormx/double_write_callback.go
@@ -0,0 +1,22 @@
+package gormx
+
+import (
+ "github.com/ecodeclub/ekit/syncx/atomicx"
+ "gorm.io/gorm"
+)
+
+type DoubleWriteCallback struct {
+ src *gorm.DB
+ dst *gorm.DB
+ pattern *atomicx.Value[string]
+}
+
+func (d *DoubleWriteCallback) create() func(db *gorm.DB) {
+ return func(db *gorm.DB) {
+ // 你这里希望完成双写
+ // 这里只有一个 db 过来,你要么是 src,要么是 dst
+ // 做不到动态切换
+ // 这里你改不了的
+ // d.src.Create(db.Statement.Model).Error
+ }
+}
diff --git a/webook/pkg/grpcx/balancer/wrr/wrr.go b/webook/pkg/grpcx/balancer/wrr/wrr.go
new file mode 100644
index 0000000000000000000000000000000000000000..df687b39eb99bfc71132310b9001c71d528f3118
--- /dev/null
+++ b/webook/pkg/grpcx/balancer/wrr/wrr.go
@@ -0,0 +1,181 @@
+package wrr
+
+import (
+ "context"
+ "google.golang.org/grpc/balancer"
+ "google.golang.org/grpc/balancer/base"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+ "io"
+ "sync"
+)
+
+const name = "custom_wrr"
+
+// balancer.Balancer 接口
+// balancer.Builder 接口
+// balancer.Picker 接口
+// base.PickerBuilder 接口
+// 你可以认为,Balancer 是 Picker 的装饰器
+func init() {
+ // NewBalancerBuilder 是帮我们把一个 Picker Builder 转化为一个 balancer.Builder
+ balancer.Register(base.NewBalancerBuilder("custom_wrr",
+ &PickerBuilder{}, base.Config{HealthCheck: false}))
+}
+
+// 传统版本的基于权重的负载均衡算法
+
+type PickerBuilder struct {
+}
+
+func (p *PickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
+ conns := make([]*conn, 0, len(info.ReadySCs))
+ // sc => SubConn
+ // sci => SubConnInfo
+ for sc, sci := range info.ReadySCs {
+ cc := &conn{
+ cc: sc,
+ }
+ md, ok := sci.Address.Metadata.(map[string]any)
+ if ok {
+ weightVal := md["weight"]
+ weight, _ := weightVal.(float64)
+ cc.weight = int(weight)
+ //group, _ := md["group"]
+ //cc.group =group
+ cc.labels = md["labels"].([]string)
+ }
+
+ if cc.weight == 0 {
+ // 可以给个默认值
+ cc.weight = 10
+ }
+ cc.currentWeight = cc.weight
+ conns = append(conns, cc)
+ }
+ return &Picker{
+ conns: conns,
+ }
+}
+
+type Picker struct {
+ // 这个才是真的执行负载均衡的地方
+ conns []*conn
+ mutex sync.Mutex
+}
+
+// Pick 在这里实现基于权重的负载均衡算法
+func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
+ p.mutex.Lock()
+ defer p.mutex.Unlock()
+ if len(p.conns) == 0 {
+ // 没有候选节点
+ return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
+ }
+
+ var total int
+ var maxCC *conn
+ // 要计算当前权重
+ //label := info.Ctx.Value("label")
+ for _, cc := range p.conns {
+ if !cc.available {
+ continue
+ }
+
+ // 如果要是 cc 里面的所有标签都不包含这个 label ,就跳过
+
+ // 性能最好就是在 cc 上用原子操作
+ // 但是筛选结果不会严格符合 WRR 算法
+ // 整体效果可以
+ //cc.lock.Lock()
+ total += cc.weight
+ cc.currentWeight = cc.currentWeight + cc.weight
+ if maxCC == nil || cc.currentWeight > maxCC.currentWeight {
+ maxCC = cc
+ }
+ //cc.lock.Unlock()
+ }
+
+ // 更新
+ maxCC.currentWeight = maxCC.currentWeight - total
+ // maxCC 就是挑出来的
+ return balancer.PickResult{
+ SubConn: maxCC.cc,
+ Done: func(info balancer.DoneInfo) {
+ // 很多动态算法,根据调用结果来调整权重,就在这里
+ err := info.Err
+ if err == nil {
+ // 你可以考虑增加权重 weight/currentWeight
+ return
+ }
+ switch err {
+ // 一般是主动取消,你没必要去调
+ case context.Canceled:
+ return
+ case context.DeadlineExceeded:
+ case io.EOF, io.ErrUnexpectedEOF:
+ // 基本可以认为这个节点已经崩了
+ maxCC.available = true
+ // 可以考虑降低权重
+ default:
+ st, ok := status.FromError(err)
+ if ok {
+ code := st.Code()
+ switch code {
+ case codes.Unavailable:
+ // 这里可能表达的是熔断
+ // 这里就要考虑挪走该节点,这个节点已经不可用了
+ // 注意并发,这里可以用原子操作
+ maxCC.available = false
+ go func() {
+ // 你要开一个额外的 goroutine 去探活
+ // 借助 health check
+ // for 循环
+ if p.healthCheck(maxCC) {
+ // 放回来
+ maxCC.available = true
+ // 最好加点流量控制的措施
+ // maxCC.currentWeight
+ // 要求下一次选中 maxCC 的时候
+ // 掷骰子
+ }
+ }()
+ case codes.ResourceExhausted:
+ // 这里可能表达的是限流
+ // 你可以挪走
+ // 也可以留着,留着的话,你就要降低权重,
+ // 最好是 currentWeight 和 weight 都调低
+ // 减少它被选中的概率
+
+ // 加一个错误码表达降级
+ }
+ }
+ }
+ },
+ }, nil
+}
+
+func (p *Picker) healthCheck(cc *conn) bool {
+ // 调用 grpc 内置的那个 health check 接口
+ return true
+}
+
+// conn 代表节点
+type conn struct {
+ // (初始)权重
+ weight int
+ labels []string
+ // 有效权重
+ //efficientWeight int
+ currentWeight int
+
+ //lock sync.Mutex
+
+ // 真正的,grpc 里面的代表一个节点的表达
+ cc balancer.SubConn
+
+ available bool
+
+ // 假如有 vip 或者非 vip
+ group string
+}
diff --git a/webook/pkg/grpcx/interceptors/builder.go b/webook/pkg/grpcx/interceptors/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..3d19e5c8778989d96c08526c0860aa7e5d9df8cf
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/builder.go
@@ -0,0 +1,54 @@
+package interceptors
+
+import (
+ "context"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/peer"
+ "net"
+ "strings"
+)
+
+type Builder struct {
+}
+
+// PeerName 获取对端应用名称
+func (b *Builder) PeerName(ctx context.Context) string {
+ return b.grpcHeaderValue(ctx, "app")
+}
+
+// PeerIP 获取对端ip
+func (b *Builder) PeerIP(ctx context.Context) string {
+ // 如果在 ctx 里面传入。或者说客户端里面设置了,就直接用它设置的
+ // 有些时候你经过网关之类的东西,就需要客户端主动设置,防止后面拿到网关的 IP
+ clientIP := b.grpcHeaderValue(ctx, "client-ip")
+ if clientIP != "" {
+ return clientIP
+ }
+
+ // 从grpc里取对端ip
+ pr, ok2 := peer.FromContext(ctx)
+ if !ok2 {
+ return ""
+ }
+ if pr.Addr == net.Addr(nil) {
+ return ""
+ }
+ addSlice := strings.Split(pr.Addr.String(), ":")
+ if len(addSlice) > 1 {
+ return addSlice[0]
+ }
+ return ""
+}
+
+func (b *Builder) grpcHeaderValue(ctx context.Context, key string) string {
+ if key == "" {
+ return ""
+ }
+ // 你如果要在 grpc 客户端和服务端之间传递元数据,就用这个
+ // 服务端接收数据,读取数据的用法
+ md, ok := metadata.FromIncomingContext(ctx)
+ if !ok {
+ return ""
+ }
+ return strings.Join(md.Get(key), ";")
+}
diff --git a/webook/pkg/grpcx/interceptors/circuitbreaker/builder.go b/webook/pkg/grpcx/interceptors/circuitbreaker/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..e9e96349a40c9983330ce253ed608fb848ade166
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/circuitbreaker/builder.go
@@ -0,0 +1,78 @@
+package circuitbreaker
+
+import (
+ "context"
+ "github.com/go-kratos/aegis/circuitbreaker"
+ "google.golang.org/grpc"
+ rand2 "math/rand"
+)
+
+type InterceptorBuilder struct {
+ breaker circuitbreaker.CircuitBreaker
+
+ // 设置标记位
+ // 假如说我们考虑使用随机数 + 阈值的回复方式
+ // 触发熔断的时候,直接将 threshold 置为0
+ // 后续等一段时间,将 theshold 调整为 1,判定请求有没有问题
+ threshold int
+}
+
+func (b *InterceptorBuilder) BuildServerInterceptor() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo,
+ handler grpc.UnaryHandler) (resp any, err error) {
+ if b.breaker.Allow() == nil {
+ resp, err = handler(ctx, req)
+ // 借助这个区判定是不是业务错误
+ //s, ok :=status.FromError(err)
+ //if s != nil && s.Code() == codes.Unavailable {
+ // b.breaker.MarkFailed()
+ //} else {
+ //
+ //}
+ if err != nil {
+ // 进一步区别是不是系统错
+ // 我这边没有区别业务错误和系统错误
+ b.breaker.MarkFailed()
+ } else {
+ b.breaker.MarkSuccess()
+ }
+ }
+
+ b.breaker.MarkFailed()
+ // 触发了熔断器
+ return nil, err
+ }
+}
+
+func (b *InterceptorBuilder) BuildServerInterceptorV1() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
+ if !b.allow() {
+ b.threshold = b.threshold / 2
+ // 这里就是触发了熔断
+ //b.threshold = 0
+ //time.AfterFunc(time.Minute, func() {
+ // b.threshold = 1
+ //})
+ }
+ // 下面就是随机数判定
+ rand := rand2.Intn(100)
+ if rand <= b.threshold {
+ resp, err = handler(ctx, req)
+ if err == nil && b.threshold != 0 {
+ // 你要考虑调大 threshold
+ } else if b.threshold != 0 {
+ // 你要考虑调小 threshold
+ }
+ }
+ return
+ }
+}
+
+func (b *InterceptorBuilder) allow() bool {
+ // 这边就套用我们之前在短信里面讲的,判定节点是否健康的各种做法
+ // 从prometheus 里面拿数据判定
+ // prometheus.DefaultGatherer.Gather()
+ return false
+}
diff --git a/webook/pkg/grpcx/interceptors/logging/interceptor.go b/webook/pkg/grpcx/interceptors/logging/interceptor.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a0d295e562f06446586dc21aa00eed83b01cc3a
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/logging/interceptor.go
@@ -0,0 +1,80 @@
+package logging
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx/interceptors"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+ "runtime"
+ "time"
+)
+
+type InterceptorBuilder struct {
+ // 如果你要非常通用
+ l logger.LoggerV1
+ //fn func(msg string, fields...logger.Field)
+ interceptors.Builder
+
+ reqBody bool
+ respBody bool
+}
+
+func (i *InterceptorBuilder) BuildClient() grpc.UnaryClientInterceptor {
+ return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+ //start := time.Now()
+ //event := "normal"
+ //defer func() {
+ // // 照着抄
+ //}()
+ return invoker(ctx, method, req, reply, cc, opts...)
+ }
+}
+
+func (i *InterceptorBuilder) Build() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context,
+ req any, info *grpc.UnaryServerInfo,
+ handler grpc.UnaryHandler) (resp any, err error) {
+ start := time.Now()
+ var event = "normal"
+ defer func() {
+ // 执行时间
+ duration := time.Since(start)
+ if rec := recover(); rec != nil {
+ switch recType := rec.(type) {
+ case error:
+ err = recType
+ default:
+ err = fmt.Errorf("%v", rec)
+ }
+ stack := make([]byte, 4096)
+ stack = stack[:runtime.Stack(stack, true)]
+ event = "recover"
+ err = status.New(codes.Internal, "panic, err "+err.Error()).Err()
+ }
+ fields := []logger.Field{
+ logger.Int64("cost", duration.Milliseconds()),
+ logger.String("type", "unary"),
+ logger.String("method", info.FullMethod),
+ logger.String("event", event),
+ // 这一个部分,是需要你的客户端配合的,
+ // 你需要知道是哪一个业务调用过来的
+ // 是哪个业务的哪个节点过来的
+ logger.String("peer", i.PeerName(ctx)),
+ logger.String("peer_ip", i.PeerIP(ctx)),
+ }
+ if err != nil {
+ st, _ := status.FromError(err)
+ fields = append(fields, logger.String("code",
+ st.Code().String()),
+ logger.String("code_msg", st.Message()))
+ }
+
+ i.l.Info("RPC请求", fields...)
+ }()
+ resp, err = handler(ctx, req)
+ return
+ }
+}
diff --git a/webook/pkg/grpcx/interceptors/promethues/builder.go b/webook/pkg/grpcx/interceptors/promethues/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..89cdf349437e8920150dbeeb32586711b3f74b25
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/promethues/builder.go
@@ -0,0 +1,62 @@
+package promethues
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx/interceptors"
+ "github.com/prometheus/client_golang/prometheus"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/status"
+ "strings"
+ "time"
+)
+
+type InterceptorBuilder struct {
+ // 用设呢?
+ Namespace string
+ Subsystem string
+ interceptors.Builder
+}
+
+func (b *InterceptorBuilder) BuildServer() grpc.UnaryServerInterceptor {
+ summary := prometheus.NewSummaryVec(
+ prometheus.SummaryOpts{
+ Namespace: b.Namespace,
+ Subsystem: b.Subsystem,
+ Name: "server_handle_seconds",
+ Objectives: map[float64]float64{
+ 0.5: 0.01,
+ 0.9: 0.01,
+ 0.95: 0.01,
+ 0.99: 0.001,
+ 0.999: 0.0001,
+ },
+ }, []string{"type", "service", "method", "peer", "code"})
+ prometheus.MustRegister(summary)
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo,
+ handler grpc.UnaryHandler) (resp any, err error) {
+ start := time.Now()
+ defer func() {
+ s, m := b.splitMethodName(info.FullMethod)
+ duration := float64(time.Since(start).Milliseconds())
+ if err == nil {
+ summary.WithLabelValues("unary", s, m, b.PeerName(ctx), "OK").Observe(duration)
+ } else {
+ st, _ := status.FromError(err)
+ summary.WithLabelValues("unary", s, m, b.PeerName(ctx), st.Code().String()).Observe(duration)
+ }
+ }()
+ resp, err = handler(ctx, req)
+ return
+ }
+}
+
+func (b *InterceptorBuilder) splitMethodName(fullMethodName string) (string, string) {
+ // /UserService/GetByID
+ // /user.v1.UserService/GetByID
+ fullMethodName = strings.TrimPrefix(fullMethodName, "/") // remove leading slash
+ if i := strings.Index(fullMethodName, "/"); i >= 0 {
+ return fullMethodName[:i], fullMethodName[i+1:]
+ }
+ return "unknown", "unknown"
+}
diff --git a/webook/pkg/grpcx/interceptors/ratelimit/interceptor.go b/webook/pkg/grpcx/interceptors/ratelimit/interceptor.go
new file mode 100644
index 0000000000000000000000000000000000000000..609dbce134fceaed45e15674ed11dea86f2d694f
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/ratelimit/interceptor.go
@@ -0,0 +1,106 @@
+package ratelimit
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/ratelimit"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+ "strings"
+)
+
+type InterceptorBuilder struct {
+ limiter ratelimit.Limiter
+ key string
+ l logger.LoggerV1
+
+ // key 是 FullMethod, value 是默认值的 json
+ //defaultValueMap map[string]string
+ //defaultValueFuncMap map[string]func() any
+}
+
+// NewInterceptorBuilder key: user-service
+// "limiter:service:user" 整个应用、集群限流
+// "limiter:service:user:UserService" user 里面的 UserService 限流
+func NewInterceptorBuilder(limiter ratelimit.Limiter, key string, l logger.LoggerV1) *InterceptorBuilder {
+ return &InterceptorBuilder{limiter: limiter, key: key, l: l}
+}
+
+func (b *InterceptorBuilder) BuildServerInterceptor() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
+ limited, err := b.limiter.Limit(ctx, b.key)
+ if err != nil {
+ // err 不为nil,你要考虑你用保守的,还是用激进的策略
+ // 这是保守的策略
+ b.l.Error("判定限流出现问题", logger.Error(err))
+ return nil, status.Errorf(codes.ResourceExhausted, "触发限流")
+
+ // 这是激进的策略
+ // return handler(ctx, req)
+ }
+ if limited {
+ //defVal, ok := b.defaultValueMap[info.FullMethod]
+ //if ok {
+ // err = json.Unmarshal([]byte(defVal), &resp)
+ // return defVal, err
+ //}
+ return nil, status.Errorf(codes.ResourceExhausted, "触发限流")
+ }
+ return handler(ctx, req)
+ }
+}
+
+// BuildServerInterceptorV1 用来配合后面业务的
+func (b *InterceptorBuilder) BuildServerInterceptorV1() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
+ limited, err := b.limiter.Limit(ctx, b.key)
+ if err != nil || limited {
+ ctx = context.WithValue(ctx, "limited", "true")
+ }
+
+ return handler(ctx, req)
+ }
+}
+
+func (b *InterceptorBuilder) BuildClientInterceptor() grpc.UnaryClientInterceptor {
+ return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+ limited, err := b.limiter.Limit(ctx, b.key)
+ if err != nil {
+ // err 不为nil,你要考虑你用保守的,还是用激进的策略
+ // 这是保守的策略
+ b.l.Error("判定限流出现问题", logger.Error(err))
+ return status.Errorf(codes.ResourceExhausted, "触发限流")
+ // 这是激进的策略
+ // return handler(ctx, req)
+ }
+ if limited {
+ return status.Errorf(codes.ResourceExhausted, "触发限流")
+ }
+ return invoker(ctx, method, req, reply, cc, opts...)
+ }
+}
+
+// 服务级别限流
+func (b *InterceptorBuilder) BuildServerInterceptorService() grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any,
+ info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
+ if strings.HasPrefix(info.FullMethod, "/UserService") {
+ limited, err := b.limiter.Limit(ctx, "limiter:service:user:UserService")
+ if err != nil {
+ // err 不为nil,你要考虑你用保守的,还是用激进的策略
+ // 这是保守的策略
+ b.l.Error("判定限流出现问题", logger.Error(err))
+ return nil, status.Errorf(codes.ResourceExhausted, "触发限流")
+ // 这是激进的策略
+ // return handler(ctx, req)
+ }
+ if limited {
+ return nil, status.Errorf(codes.ResourceExhausted, "触发限流")
+ }
+ }
+ return handler(ctx, req)
+ }
+}
diff --git a/webook/pkg/grpcx/interceptors/trace/builder.go b/webook/pkg/grpcx/interceptors/trace/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..15dba8a9f54940df520bf85ec3ce326d43c21d1a
--- /dev/null
+++ b/webook/pkg/grpcx/interceptors/trace/builder.go
@@ -0,0 +1,157 @@
+package trace
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx/interceptors"
+ "github.com/go-kratos/kratos/v2/errors"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/codes"
+ "go.opentelemetry.io/otel/propagation"
+ semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
+ "go.opentelemetry.io/otel/trace"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/metadata"
+)
+
+type InterceptorBuilder struct {
+ tracer trace.Tracer
+ propagator propagation.TextMapPropagator
+ interceptors.Builder
+}
+
+func NewInterceptorBuilder(tracer trace.Tracer, propagator propagation.TextMapPropagator) *InterceptorBuilder {
+ return &InterceptorBuilder{tracer: tracer, propagator: propagator}
+}
+
+func (b *InterceptorBuilder) BuildClient() grpc.UnaryClientInterceptor {
+ propagator := b.propagator
+ if propagator == nil {
+ // 这个是全局
+ propagator = otel.GetTextMapPropagator()
+ }
+ tracer := b.tracer
+ if tracer == nil {
+ tracer = otel.Tracer("gitee.com/geekbang/basic-go/webook/pkg/grpcx/interceptors/trace")
+ }
+ attrs := []attribute.KeyValue{
+ semconv.RPCSystemKey.String("grpc"),
+ attribute.Key("rpc.grpc.kind").String("unary"),
+ attribute.Key("rpc.component").String("client"),
+ }
+ return func(ctx context.Context, method string,
+ req, reply any, cc *grpc.ClientConn,
+ invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) {
+ ctx, span := tracer.Start(ctx, method,
+ trace.WithAttributes(attrs...),
+ trace.WithSpanKind(trace.SpanKindClient))
+ defer span.End()
+ defer func() {
+ if err != nil {
+ span.RecordError(err)
+ if e := errors.FromError(err); e != nil {
+ span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(e.Code)))
+ }
+ span.SetStatus(codes.Error, err.Error())
+ } else {
+ span.SetStatus(codes.Ok, "OK")
+ }
+ span.End()
+ }()
+ // inject 过程
+ // 要把跟 trace 有关的链路元数据,传递到服务端
+ ctx = inject(ctx, propagator)
+ err = invoker(ctx, method, req, reply, cc, opts...)
+ return
+ }
+}
+
+func (b *InterceptorBuilder) BuildServer() grpc.UnaryServerInterceptor {
+ propagator := b.propagator
+ if propagator == nil {
+ // 这个是全局
+ propagator = otel.GetTextMapPropagator()
+ }
+ tracer := b.tracer
+ if tracer == nil {
+ tracer = otel.Tracer("gitee.com/geekbang/basic-go/webook/pkg/grpcx/interceptors/trace")
+ }
+ attrs := []attribute.KeyValue{
+ semconv.RPCSystemKey.String("grpc"),
+ attribute.Key("rpc.grpc.kind").String("unary"),
+ attribute.Key("rpc.component").String("server"),
+ }
+ return func(ctx context.Context,
+ req any, info *grpc.UnaryServerInfo,
+ handler grpc.UnaryHandler) (resp any, err error) {
+ ctx = extract(ctx, propagator)
+ ctx, span := tracer.Start(ctx, info.FullMethod,
+ trace.WithSpanKind(trace.SpanKindServer),
+ trace.WithAttributes(attrs...))
+ defer span.End()
+ span.SetAttributes(
+ semconv.RPCMethodKey.String(info.FullMethod),
+ semconv.NetPeerNameKey.String(b.PeerName(ctx)),
+ attribute.Key("net.peer.ip").String(b.PeerIP(ctx)),
+ )
+ defer func() {
+ // 就要结束了
+ if err != nil {
+ span.RecordError(err)
+ } else {
+ span.SetStatus(codes.Ok, "OK")
+ }
+ }()
+ resp, err = handler(ctx, req)
+ return
+ }
+}
+
+func inject(ctx context.Context, propagators propagation.TextMapPropagator) context.Context {
+ // 先看 ctx 里面有没有元数据
+ md, ok := metadata.FromOutgoingContext(ctx)
+ if !ok {
+ md = metadata.New(map[string]string{})
+ }
+ // 把元数据放回去 ctx,具体怎么放,放什么内容,由 propagator 决定
+ propagators.Inject(ctx, GrpcHeaderCarrier(md))
+ // 为什么还要把这个搞回去?
+ return metadata.NewOutgoingContext(ctx, md)
+}
+
+func extract(ctx context.Context, p propagation.TextMapPropagator) context.Context {
+ // 拿到客户端过来的链路元数据
+ // "md": map[string]string
+ md, ok := metadata.FromIncomingContext(ctx)
+ if !ok {
+ md = metadata.New(map[string]string{})
+ }
+ // 把这个 md 注入到 ctx 里面
+ // 根据你采用 zipkin 或者 jeager,它的注入方式不同
+ return p.Extract(ctx, GrpcHeaderCarrier(md))
+}
+
+type GrpcHeaderCarrier metadata.MD
+
+// Get returns the value associated with the passed key.
+func (mc GrpcHeaderCarrier) Get(key string) string {
+ vals := metadata.MD(mc).Get(key)
+ if len(vals) > 0 {
+ return vals[0]
+ }
+ return ""
+}
+
+// Set stores the key-value pair.
+func (mc GrpcHeaderCarrier) Set(key string, value string) {
+ metadata.MD(mc).Set(key, value)
+}
+
+// Keys lists the keys stored in this carrier.
+func (mc GrpcHeaderCarrier) Keys() []string {
+ keys := make([]string, 0, len(mc))
+ for k := range metadata.MD(mc) {
+ keys = append(keys, k)
+ }
+ return keys
+}
diff --git a/webook/pkg/grpcx/server.go b/webook/pkg/grpcx/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..9df01091334d3c29077813c5b37143a372c33446
--- /dev/null
+++ b/webook/pkg/grpcx/server.go
@@ -0,0 +1,92 @@
+package grpcx
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/netx"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "go.etcd.io/etcd/client/v3/naming/endpoints"
+ "google.golang.org/grpc"
+ "net"
+ "strconv"
+ "time"
+)
+
+type Server struct {
+ *grpc.Server
+ Port int
+ // ETCD 服务注册租约 TTL
+ EtcdTTL int64
+ EtcdClient *clientv3.Client
+ etcdManager endpoints.Manager
+ etcdKey string
+ cancel func()
+ Name string
+ L logger.LoggerV1
+}
+
+// Serve 启动服务器并且阻塞
+func (s *Server) Serve() error {
+ // 初始化一个控制整个过程的 ctx
+ // 你也可以考虑让外面传进来,这样的话就是 main 函数自己去控制了
+ ctx, cancel := context.WithCancel(context.Background())
+ s.cancel = cancel
+ port := strconv.Itoa(s.Port)
+ l, err := net.Listen("tcp", ":"+port)
+ if err != nil {
+ return err
+ }
+ // 要先确保启动成功,再注册服务
+ err = s.register(ctx, port)
+ if err != nil {
+ return err
+ }
+ return s.Server.Serve(l)
+}
+
+func (s *Server) register(ctx context.Context, port string) error {
+ cli := s.EtcdClient
+ serviceName := "service/" + s.Name
+ em, err := endpoints.NewManager(cli,
+ serviceName)
+ if err != nil {
+ return err
+ }
+ s.etcdManager = em
+ ip := netx.GetOutboundIP()
+ s.etcdKey = serviceName + "/" + ip
+ addr := ip + ":" + port
+ leaseResp, err := cli.Grant(ctx, s.EtcdTTL)
+ // 开启续约
+ ch, err := cli.KeepAlive(ctx, leaseResp.ID)
+ if err != nil {
+ return err
+ }
+ go func() {
+ // 可以预期,当我们的 cancel 被调用的时候,就会退出这个循环
+ for chResp := range ch {
+ s.L.Debug("续约:", logger.String("resp", chResp.String()))
+ }
+ }()
+ // metadata 我们这里没啥要提供的
+ return em.AddEndpoint(ctx, s.etcdKey,
+ endpoints.Endpoint{Addr: addr}, clientv3.WithLease(leaseResp.ID))
+}
+
+func (s *Server) Close() error {
+ s.cancel()
+ if s.etcdManager != nil {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ err := s.etcdManager.DeleteEndpoint(ctx, s.etcdKey)
+ if err != nil {
+ return err
+ }
+ }
+ err := s.EtcdClient.Close()
+ if err != nil {
+ return err
+ }
+ s.Server.GracefulStop()
+ return nil
+}
diff --git a/webook/pkg/logger/fields.go b/webook/pkg/logger/fields.go
new file mode 100644
index 0000000000000000000000000000000000000000..293c549edd009bf0fa2c1495ecbac6cef7a56ead
--- /dev/null
+++ b/webook/pkg/logger/fields.go
@@ -0,0 +1,29 @@
+package logger
+
+func String(key, val string) Field {
+ return Field{
+ Key: key,
+ Value: val,
+ }
+}
+
+func Int32(key string, val int32) Field {
+ return Field{
+ Key: key,
+ Value: val,
+ }
+}
+
+func Int64(key string, val int64) Field {
+ return Field{
+ Key: key,
+ Value: val,
+ }
+}
+
+func Error(err error) Field {
+ return Field{
+ Key: "error",
+ Value: err,
+ }
+}
diff --git a/webook/pkg/logger/global_instance.go b/webook/pkg/logger/global_instance.go
new file mode 100644
index 0000000000000000000000000000000000000000..34f42f8993772ce8ccbd0c34d16ae03ad98a92a7
--- /dev/null
+++ b/webook/pkg/logger/global_instance.go
@@ -0,0 +1,21 @@
+package logger
+
+import "sync"
+
+var gl LoggerV1
+var lMutex sync.RWMutex
+
+func SetGlobalLogger(l LoggerV1) {
+ lMutex.Lock()
+ defer lMutex.Unlock()
+ gl = l
+}
+
+func L() LoggerV1 {
+ lMutex.RLock()
+ g := gl
+ lMutex.RUnlock()
+ return g
+}
+
+var GL LoggerV1 = &NopLogger{}
diff --git a/webook/pkg/logger/nop.go b/webook/pkg/logger/nop.go
new file mode 100644
index 0000000000000000000000000000000000000000..4f8aea13d5d955442c18129f303e56fa82d522d1
--- /dev/null
+++ b/webook/pkg/logger/nop.go
@@ -0,0 +1,19 @@
+package logger
+
+type NopLogger struct {
+}
+
+func NewNoOpLogger() *NopLogger {
+ return &NopLogger{}
+}
+func (n *NopLogger) Debug(msg string, args ...Field) {
+}
+
+func (n *NopLogger) Info(msg string, args ...Field) {
+}
+
+func (n *NopLogger) Warn(msg string, args ...Field) {
+}
+
+func (n *NopLogger) Error(msg string, args ...Field) {
+}
diff --git a/webook/pkg/logger/types.go b/webook/pkg/logger/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..78722866ce821c415888024fbf321ba687a84dea
--- /dev/null
+++ b/webook/pkg/logger/types.go
@@ -0,0 +1,49 @@
+package logger
+
+type Logger interface {
+ Debug(msg string, args ...any)
+ Info(msg string, args ...any)
+ Warn(msg string, args ...any)
+ Error(msg string, args ...any)
+}
+
+func LoggerExample() {
+ var l Logger
+ phone := "152xxxx1234"
+ l.Info("用户未注册,手机号码是 %s", phone)
+}
+
+type LoggerV1 interface {
+ Debug(msg string, args ...Field)
+ Info(msg string, args ...Field)
+ Warn(msg string, args ...Field)
+ Error(msg string, args ...Field)
+}
+
+type Field struct {
+ Key string
+ Value any
+}
+
+func LoggerV1Example() {
+ var l LoggerV1
+ phone := "152xxxx1234"
+ l.Info("用户未注册", Field{
+ Key: "phone",
+ Value: phone,
+ })
+}
+
+type LoggerV2 interface {
+ // args 必须是偶数,并且按照 key-value, key-value 来组织
+ Debug(msg string, args ...any)
+ Info(msg string, args ...any)
+ Warn(msg string, args ...any)
+ Error(msg string, args ...any)
+}
+
+func LoggerV2Example() {
+ var l LoggerV2
+ phone := "152xxxx1234"
+ l.Info("用户未注册", "phone", phone)
+}
diff --git a/webook/pkg/logger/zap_logger.go b/webook/pkg/logger/zap_logger.go
new file mode 100644
index 0000000000000000000000000000000000000000..76c89da8d9e01e9e348ada4105550430c96147e7
--- /dev/null
+++ b/webook/pkg/logger/zap_logger.go
@@ -0,0 +1,37 @@
+package logger
+
+import "go.uber.org/zap"
+
+type ZapLogger struct {
+ l *zap.Logger
+}
+
+func NewZapLogger(l *zap.Logger) *ZapLogger {
+ return &ZapLogger{
+ l: l,
+ }
+}
+
+func (z *ZapLogger) Debug(msg string, args ...Field) {
+ z.l.Debug(msg, z.toZapFields(args)...)
+}
+
+func (z *ZapLogger) Info(msg string, args ...Field) {
+ z.l.Info(msg, z.toZapFields(args)...)
+}
+
+func (z *ZapLogger) Warn(msg string, args ...Field) {
+ z.l.Warn(msg, z.toZapFields(args)...)
+}
+
+func (z *ZapLogger) Error(msg string, args ...Field) {
+ z.l.Error(msg, z.toZapFields(args)...)
+}
+
+func (z *ZapLogger) toZapFields(args []Field) []zap.Field {
+ res := make([]zap.Field, 0, len(args))
+ for _, arg := range args {
+ res = append(res, zap.Any(arg.Key, arg.Value))
+ }
+ return res
+}
diff --git a/webook/pkg/migrator/doc.go b/webook/pkg/migrator/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..0fd4060401ccd827a71cbdd3617584edf85bee52
--- /dev/null
+++ b/webook/pkg/migrator/doc.go
@@ -0,0 +1,2 @@
+// Package migrator 是用于数据迁移的东西
+package migrator
diff --git a/webook/pkg/migrator/events/fixer/consumer.go b/webook/pkg/migrator/events/fixer/consumer.go
new file mode 100644
index 0000000000000000000000000000000000000000..b3bc52d55a4a308120879282a02315f5adf9ae1b
--- /dev/null
+++ b/webook/pkg/migrator/events/fixer/consumer.go
@@ -0,0 +1,75 @@
+package fixer
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/fixer"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "gorm.io/gorm"
+ "time"
+)
+
+type Consumer[T migrator.Entity] struct {
+ client sarama.Client
+ l logger.LoggerV1
+ srcFirst *fixer.OverrideFixer[T]
+ dstFirst *fixer.OverrideFixer[T]
+ topic string
+}
+
+func NewConsumer[T migrator.Entity](
+ client sarama.Client,
+ l logger.LoggerV1,
+ topic string,
+ src *gorm.DB,
+ dst *gorm.DB) (*Consumer[T], error) {
+ srcFirst, err := fixer.NewOverrideFixer[T](src, dst)
+ if err != nil {
+ return nil, err
+ }
+ dstFirst, err := fixer.NewOverrideFixer[T](dst, src)
+ if err != nil {
+ return nil, err
+ }
+ return &Consumer[T]{
+ client: client,
+ l: l,
+ srcFirst: srcFirst,
+ dstFirst: dstFirst,
+ topic: topic,
+ }, nil
+}
+
+// Start 这边就是自己启动 goroutine 了
+func (r *Consumer[T]) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("migrator-fix",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{r.topic},
+ saramax.NewHandler[events.InconsistentEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (r *Consumer[T]) Consume(msg *sarama.ConsumerMessage, t events.InconsistentEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ switch t.Direction {
+ case "SRC":
+ return r.srcFirst.Fix(ctx, t.ID)
+ case "DST":
+ return r.dstFirst.Fix(ctx, t.ID)
+ }
+ return errors.New("未知的校验方向")
+}
diff --git a/webook/pkg/migrator/events/inconsistent.go b/webook/pkg/migrator/events/inconsistent.go
new file mode 100644
index 0000000000000000000000000000000000000000..bc9bc1b55737c023c65cc480d4b6d88e38360fcf
--- /dev/null
+++ b/webook/pkg/migrator/events/inconsistent.go
@@ -0,0 +1,28 @@
+package events
+
+type InconsistentEvent struct {
+ ID int64
+ // 用什么来修,取值为 SRC,意味着,以源表为准,取值为 DST,以目标表为准
+ Direction string
+ // 有些时候,一些观测,或者一些第三方,需要知道,是什么引起的不一致
+ // 因为他要去 DEBUG
+ // 这个是可选的
+ Type string
+
+ // 事件里面带 base 的数据
+ // 修复数据用这里的去修,这种做法是不行的,因为有严重的并发问题
+ Columns map[string]any
+}
+
+const (
+ // InconsistentEventTypeTargetMissing 校验的目标数据,缺了这一条
+ InconsistentEventTypeTargetMissing = "target_missing"
+ // InconsistentEventTypeNEQ 不相等
+ InconsistentEventTypeNEQ = "neq"
+ InconsistentEventTypeBaseMissing = "base_missing"
+)
+
+//type Fixer struct {
+// base *gorm.DB
+// target *gorm.DB
+//}
diff --git a/webook/pkg/migrator/events/mocks/producer.mock.go b/webook/pkg/migrator/events/mocks/producer.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..857e89748d6b7b0c24d2e05a0219b0664847b288
--- /dev/null
+++ b/webook/pkg/migrator/events/mocks/producer.mock.go
@@ -0,0 +1,54 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: producer.go
+//
+// Generated by this command:
+//
+// mockgen -source=producer.go -package=evtmocks -destination=mocks/producer.mock.go Producer
+//
+// Package evtmocks is a generated GoMock package.
+package evtmocks
+
+import (
+ context "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockProducer is a mock of Producer interface.
+type MockProducer struct {
+ ctrl *gomock.Controller
+ recorder *MockProducerMockRecorder
+}
+
+// MockProducerMockRecorder is the mock recorder for MockProducer.
+type MockProducerMockRecorder struct {
+ mock *MockProducer
+}
+
+// NewMockProducer creates a new mock instance.
+func NewMockProducer(ctrl *gomock.Controller) *MockProducer {
+ mock := &MockProducer{ctrl: ctrl}
+ mock.recorder = &MockProducerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockProducer) EXPECT() *MockProducerMockRecorder {
+ return m.recorder
+}
+
+// ProduceInconsistentEvent mocks base method.
+func (m *MockProducer) ProduceInconsistentEvent(ctx context.Context, event events.InconsistentEvent) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ProduceInconsistentEvent", ctx, event)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ProduceInconsistentEvent indicates an expected call of ProduceInconsistentEvent.
+func (mr *MockProducerMockRecorder) ProduceInconsistentEvent(ctx, event any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProduceInconsistentEvent", reflect.TypeOf((*MockProducer)(nil).ProduceInconsistentEvent), ctx, event)
+}
diff --git a/webook/pkg/migrator/events/producer.go b/webook/pkg/migrator/events/producer.go
new file mode 100644
index 0000000000000000000000000000000000000000..55b58c8e70d500bf8e816696d79963ee1d988afa
--- /dev/null
+++ b/webook/pkg/migrator/events/producer.go
@@ -0,0 +1,36 @@
+package events
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/IBM/sarama"
+)
+
+//go:generate mockgen -source=producer.go -package=evtmocks -destination=mocks/producer.mock.go Producer
+type Producer interface {
+ ProduceInconsistentEvent(ctx context.Context, event InconsistentEvent) error
+}
+
+type SaramaProducer struct {
+ p sarama.SyncProducer
+ topic string
+}
+
+func NewSaramaProducer(
+ p sarama.SyncProducer,
+ topic string) *SaramaProducer {
+ return &SaramaProducer{p: p, topic: topic}
+}
+
+func (s *SaramaProducer) ProduceInconsistentEvent(ctx context.Context,
+ event InconsistentEvent) error {
+ data, err := json.Marshal(event)
+ if err != nil {
+ return err
+ }
+ _, _, err = s.p.SendMessage(&sarama.ProducerMessage{
+ Topic: s.topic,
+ Value: sarama.ByteEncoder(data),
+ })
+ return err
+}
diff --git a/webook/pkg/migrator/fixer/fix.go b/webook/pkg/migrator/fixer/fix.go
new file mode 100644
index 0000000000000000000000000000000000000000..57c33b1f98ee204d50e215f5f20a3cdf3bbd0fba
--- /dev/null
+++ b/webook/pkg/migrator/fixer/fix.go
@@ -0,0 +1,54 @@
+package fixer
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type OverrideFixer[T migrator.Entity] struct {
+ // 因为本身其实这个不涉及什么领域对象,
+ // 这里操作的不是 migrator 本身的领域对象
+ base *gorm.DB
+ target *gorm.DB
+ columns []string
+}
+
+func NewOverrideFixer[T migrator.Entity](base *gorm.DB,
+ target *gorm.DB) (*OverrideFixer[T], error) {
+ // 在这里需要查询一下数据库中究竟有哪些列
+ var t T
+ rows, err := base.Model(&t).Limit(1).Rows()
+ if err != nil {
+ return nil, err
+ }
+ columns, err := rows.Columns()
+ if err != nil {
+ return nil, err
+ }
+ return &OverrideFixer[T]{
+ base: base,
+ target: target,
+ columns: columns,
+ }, nil
+}
+
+func (o *OverrideFixer[T]) Fix(ctx context.Context, id int64) error {
+ var src T
+ // 找出数据
+ err := o.base.WithContext(ctx).Where("id = ?", id).
+ First(&src).Error
+ switch err {
+ // 找到了数据
+ case nil:
+ return o.target.Clauses(&clause.OnConflict{
+ // 我们需要 Entity 告诉我们,修复哪些数据
+ DoUpdates: clause.AssignmentColumns(o.columns),
+ }).Create(&src).Error
+ case gorm.ErrRecordNotFound:
+ return o.target.Delete("id = ?", id).Error
+ default:
+ return err
+ }
+}
diff --git a/webook/pkg/migrator/fixer/fixer.go b/webook/pkg/migrator/fixer/fixer.go
new file mode 100644
index 0000000000000000000000000000000000000000..46f57b6314e87511b35a83abc446f736e403b799
--- /dev/null
+++ b/webook/pkg/migrator/fixer/fixer.go
@@ -0,0 +1,117 @@
+//go:build live
+
+package fixer
+
+import (
+ "context"
+ "errors"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+// 这个是课堂演示
+type Fixer[T migrator.Entity] struct {
+ base *gorm.DB
+ target *gorm.DB
+ columns []string
+}
+
+// 最一了百了的写法
+// 不管三七二十一,我TM直接覆盖
+// 把 event 当成一个触发器,不依赖的 event 的具体内容(ID 必须不可变)
+// 修复这里,也改成批量??
+func (f *Fixer[T]) Fix(ctx context.Context, evt events.InconsistentEvent) error {
+ var t T
+ err := f.base.WithContext(ctx).
+ Where("id =?", evt.ID).First(&t).Error
+ switch err {
+ case nil:
+ // base 有数据
+ // 修复数据的时候,可以考虑增加 WHERE base.utime >= target.utime
+ // utime 用不了,就看有没有version 之类的,或者能够判定数据新老的
+ return f.target.WithContext(ctx).
+ Clauses(clause.OnConflict{
+ DoUpdates: clause.AssignmentColumns(f.columns),
+ }).Create(&t).Error
+ case gorm.ErrRecordNotFound:
+ // base 没了
+ return f.target.WithContext(ctx).
+ Where("id=?", evt.ID).Delete(&t).Error
+ default:
+ return err
+ }
+}
+
+// base 和 target 在校验时候的数据,到你修复的时候就变了
+func (f *Fixer[T]) FixV1(ctx context.Context, evt events.InconsistentEvent) error {
+ switch evt.Type {
+ case events.InconsistentEventTypeTargetMissing,
+ events.InconsistentEventTypeNEQ:
+ // 这边要插入
+ var t T
+ err := f.base.WithContext(ctx).
+ Where("id =?", evt.ID).First(&t).Error
+ switch err {
+ case gorm.ErrRecordNotFound:
+ // base 也删除了这条数据
+ return f.target.WithContext(ctx).
+ Where("id=?", evt.ID).Delete(new(T)).Error
+ case nil:
+ return f.target.Clauses(clause.OnConflict{
+ // 这边要更新全部列
+ DoUpdates: clause.AssignmentColumns(f.columns),
+ }).Create(&t).Error
+ default:
+ return err
+ }
+ // 这边要更新
+ case events.InconsistentEventTypeBaseMissing:
+ return f.target.WithContext(ctx).
+ Where("id=?", evt.ID).Delete(new(T)).Error
+ default:
+ return errors.New("未知的不一致类型")
+ }
+}
+
+// 一定要抓住,base 在校验时候的数据,到你修复的时候就变了
+func (f *Fixer[T]) FixV2(ctx context.Context, evt events.InconsistentEvent) error {
+ switch evt.Type {
+ case events.InconsistentEventTypeTargetMissing:
+ // 这边要插入
+ var t T
+ err := f.base.WithContext(ctx).
+ Where("id =?", evt.ID).First(&t).Error
+ switch err {
+ case gorm.ErrRecordNotFound:
+ // base 也删除了这条数据
+ return nil
+ case nil:
+ // 就在你插入的时候,双写的程序,也插入了,你就会冲突
+ return f.target.Create(&t).Error
+ default:
+ return err
+ }
+ case events.InconsistentEventTypeNEQ:
+ var t T
+ err := f.base.WithContext(ctx).
+ Where("id =?", evt.ID).First(&t).Error
+ switch err {
+ case gorm.ErrRecordNotFound:
+ // target 要删除
+ return f.target.WithContext(ctx).
+ Where("id=?", evt.ID).Delete(&t).Error
+ case nil:
+ return f.target.Updates(&t).Error
+ default:
+ return err
+ }
+ // 这边要更新
+ case events.InconsistentEventTypeBaseMissing:
+ return f.target.WithContext(ctx).
+ Where("id=?", evt.ID).Delete(new(T)).Error
+ default:
+ return errors.New("未知的不一致类型")
+ }
+}
diff --git a/webook/pkg/migrator/integration/intr_test.go b/webook/pkg/migrator/integration/intr_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c47db5b0ed4e4cf3f1a7bc33914fa840fd2ca159
--- /dev/null
+++ b/webook/pkg/migrator/integration/intr_test.go
@@ -0,0 +1,215 @@
+package integration
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ events2 "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events/mocks"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/integration/startup"
+ gorm2 "gitee.com/geekbang/basic-go/webook/pkg/migrator/validator"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/mock/gomock"
+ "gorm.io/gorm"
+ "testing"
+)
+
+type InteractiveTestSuite struct {
+ suite.Suite
+ srcDB *gorm.DB
+ intrDB *gorm.DB
+}
+
+func (i *InteractiveTestSuite) SetupSuite() {
+ i.srcDB = startup.InitSrcDB()
+ err := i.srcDB.AutoMigrate(&Interactive{})
+ assert.NoError(i.T(), err)
+ i.intrDB = startup.InitIntrDB()
+ err = i.intrDB.AutoMigrate(&Interactive{})
+ assert.NoError(i.T(), err)
+
+}
+
+func (i *InteractiveTestSuite) TearDownTest() {
+ i.srcDB.Exec("TRUNCATE TABLE interactives")
+ i.intrDB.Exec("TRUNCATE TABLE interactives")
+}
+
+func (i *InteractiveTestSuite) TestValidator() {
+ testCases := []struct {
+ name string
+ before func(t *testing.T)
+ after func(t *testing.T)
+ // 不想真的从 Kafka 里面读取数据,所以mock 一下
+ mock func(ctrl *gomock.Controller) events2.Producer
+
+ wantErr error
+ }{
+ {
+ name: "src有,但是intr没有",
+ before: func(t *testing.T) {
+ err := i.srcDB.Create(&Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 123,
+ ReadCnt: 111,
+ CollectCnt: 222,
+ LikeCnt: 333,
+ Ctime: 456,
+ Utime: 678,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ i.TearDownTest()
+ },
+ mock: func(ctrl *gomock.Controller) events2.Producer {
+ p := evtmocks.NewMockProducer(ctrl)
+ p.EXPECT().ProduceInconsistentEvent(gomock.Any(),
+ events2.InconsistentEvent{
+ Type: events2.InconsistentEventTypeTargetMissing,
+ Direction: "SRC",
+ ID: 1,
+ }).Return(nil)
+ return p
+ },
+ },
+ {
+ name: "src有,intr也有,数据相同",
+ before: func(t *testing.T) {
+ intr := &Interactive{
+ Id: 2,
+ Biz: "test",
+ BizId: 124,
+ ReadCnt: 111,
+ CollectCnt: 222,
+ LikeCnt: 333,
+ Ctime: 456,
+ Utime: 678,
+ }
+ err := i.srcDB.Create(intr).Error
+ assert.NoError(t, err)
+ err = i.intrDB.Create(intr).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ i.TearDownTest()
+ },
+ mock: func(ctrl *gomock.Controller) events2.Producer {
+ p := evtmocks.NewMockProducer(ctrl)
+ return p
+ },
+ },
+ {
+ name: "src有,intr有,但是数据不同",
+ before: func(t *testing.T) {
+ err := i.srcDB.Create(&Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 123,
+ ReadCnt: 111,
+ CollectCnt: 222,
+ LikeCnt: 333,
+ Ctime: 456,
+ Utime: 678,
+ }).Error
+ assert.NoError(t, err)
+ err = i.intrDB.Create(&Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 123,
+ ReadCnt: 111,
+ CollectCnt: 222,
+ LikeCnt: 33333333,
+ Ctime: 456,
+ Utime: 678,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ i.TearDownTest()
+ },
+ mock: func(ctrl *gomock.Controller) events2.Producer {
+ p := evtmocks.NewMockProducer(ctrl)
+ p.EXPECT().ProduceInconsistentEvent(gomock.Any(),
+ events2.InconsistentEvent{
+ Type: events2.InconsistentEventTypeNotEqual,
+ Direction: "SRC",
+ ID: 1,
+ }).Return(nil)
+ return p
+ },
+ },
+
+ {
+ name: "src没有,intr有",
+ before: func(t *testing.T) {
+ err := i.intrDB.Create(&Interactive{
+ Id: 1,
+ Biz: "test",
+ BizId: 123,
+ ReadCnt: 111,
+ CollectCnt: 222,
+ LikeCnt: 33333333,
+ Ctime: 456,
+ Utime: 678,
+ }).Error
+ assert.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ i.TearDownTest()
+ },
+ mock: func(ctrl *gomock.Controller) events2.Producer {
+ p := evtmocks.NewMockProducer(ctrl)
+ p.EXPECT().ProduceInconsistentEvent(gomock.Any(),
+ events2.InconsistentEvent{
+ Type: events2.InconsistentEventTypeBaseMissing,
+ Direction: "SRC",
+ ID: 1,
+ }).Return(nil)
+ return p
+ },
+ },
+ }
+ for _, tc := range testCases {
+ i.T().Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ tc.before(t)
+ v := gorm2.NewValidator[Interactive](i.srcDB, i.intrDB,
+ "SRC", logger.NewNoOpLogger(), tc.mock(ctrl))
+ err := v.Validate(context.Background())
+ assert.Equal(t, tc.wantErr, err)
+ tc.after(t)
+ })
+ }
+}
+
+func TestInteractive(t *testing.T) {
+ suite.Run(t, &InteractiveTestSuite{})
+}
+
+type Interactive struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ BizId int64 `gorm:"uniqueIndex:biz_type_id"`
+ Biz string `gorm:"type:varchar(128);uniqueIndex:biz_type_id"`
+ ReadCnt int64
+ CollectCnt int64
+ LikeCnt int64
+ Ctime int64
+ Utime int64
+}
+
+func (i Interactive) ID() int64 {
+ return i.Id
+}
+
+func (i Interactive) TableName() string {
+ return "interactives"
+}
+
+func (i Interactive) CompareTo(entity migrator.Entity) bool {
+ dst := entity.(migrator.Entity)
+ return i == dst
+}
diff --git a/webook/pkg/migrator/integration/startup/db.go b/webook/pkg/migrator/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..9e17a6c3975a8329e4f824a9f6200ec43030e696
--- /dev/null
+++ b/webook/pkg/migrator/integration/startup/db.go
@@ -0,0 +1,42 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+// InitSrcDB 初始化源表
+func InitSrcDB() *gorm.DB {
+ return initDB("webook")
+}
+
+func InitIntrDB() *gorm.DB {
+ return initDB("webook_intr")
+}
+
+func initDB(dbName string) *gorm.DB {
+ dsn := fmt.Sprintf("root:root@tcp(localhost:13316)/%s", dbName)
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err := gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
diff --git a/webook/pkg/migrator/integration/startup/kafka.go b/webook/pkg/migrator/integration/startup/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..a023bff11de2f4dea2d556c34a04eb684205a79b
--- /dev/null
+++ b/webook/pkg/migrator/integration/startup/kafka.go
@@ -0,0 +1,15 @@
+package startup
+
+import (
+ "github.com/IBM/sarama"
+)
+
+func InitKafka() sarama.Client {
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ client, err := sarama.NewClient([]string{"localhost:9094"}, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/pkg/migrator/integration/startup/log.go b/webook/pkg/migrator/integration/startup/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..a659ad9dbf326536df6bc5e6641a4aed105b15bc
--- /dev/null
+++ b/webook/pkg/migrator/integration/startup/log.go
@@ -0,0 +1,9 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+func InitLog() logger.LoggerV1 {
+ return logger.NewNoOpLogger()
+}
diff --git a/webook/pkg/migrator/integration/startup/wire.go b/webook/pkg/migrator/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..aed9ac5b76607ed7b0e3a78412e830e9ce5d4ac6
--- /dev/null
+++ b/webook/pkg/migrator/integration/startup/wire.go
@@ -0,0 +1,3 @@
+//go:build wireinject
+
+package startup
diff --git a/webook/pkg/migrator/integration/validator_test.go b/webook/pkg/migrator/integration/validator_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..76ab1b7282d8e05e72a611012e3dbf53ead7b701
--- /dev/null
+++ b/webook/pkg/migrator/integration/validator_test.go
@@ -0,0 +1 @@
+package integration
diff --git a/webook/pkg/migrator/migrate.go b/webook/pkg/migrator/migrate.go
new file mode 100644
index 0000000000000000000000000000000000000000..79dc8f4127355c643f8832de66847eb4981811f2
--- /dev/null
+++ b/webook/pkg/migrator/migrate.go
@@ -0,0 +1,8 @@
+package migrator
+
+type Entity interface {
+ // ID 要求返回 ID
+ ID() int64
+ // CompareTo dst 必然也是 Entity,正常来说类型是一样的
+ CompareTo(dst Entity) bool
+}
diff --git a/webook/pkg/migrator/scheduler/scheduler.go b/webook/pkg/migrator/scheduler/scheduler.go
new file mode 100644
index 0000000000000000000000000000000000000000..3f0730495cd649ecf0c67f24ad6871e8f0084fba
--- /dev/null
+++ b/webook/pkg/migrator/scheduler/scheduler.go
@@ -0,0 +1,206 @@
+package scheduler
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/gormx/connpool"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator/validator"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+ "sync"
+ "time"
+)
+
+// Scheduler 用来统一管理整个迁移过程
+// 它不是必须的,你可以理解为这是为了方便用户操作(和你理解)而引入的。
+type Scheduler[T migrator.Entity] struct {
+ lock sync.Mutex
+ src *gorm.DB
+ dst *gorm.DB
+ pool *connpool.DoubleWritePool
+ l logger.LoggerV1
+ pattern string
+ cancelFull func()
+ cancelIncr func()
+ producer events.Producer
+
+ // 如果你要允许多个全量校验同时运行
+ fulls map[string]func()
+}
+
+func NewScheduler[T migrator.Entity](
+ l logger.LoggerV1,
+ src *gorm.DB,
+ dst *gorm.DB,
+ // 这个是业务用的 DoubleWritePool
+ pool *connpool.DoubleWritePool,
+ producer events.Producer) *Scheduler[T] {
+ return &Scheduler[T]{
+ l: l,
+ src: src,
+ dst: dst,
+ pattern: connpool.PatternSrcOnly,
+ cancelFull: func() {
+ // 初始的时候,啥也不用做
+ },
+ cancelIncr: func() {
+ // 初始的时候,啥也不用做
+ },
+ pool: pool,
+ producer: producer,
+ }
+}
+
+// 这一个也不是必须的,就是你可以考虑利用配置中心,监听配置中心的变化
+// 把全量校验,增量校验做成分布式任务,利用分布式任务调度平台来调度
+func (s *Scheduler[T]) RegisterRoutes(server *gin.RouterGroup) {
+ // 将这个暴露为 HTTP 接口
+ // 你可以配上对应的 UI
+ server.POST("/src_only", ginx.Wrap(s.SrcOnly))
+ server.POST("/src_first", ginx.Wrap(s.SrcFirst))
+ server.POST("/dst_first", ginx.Wrap(s.DstFirst))
+ server.POST("/dst_only", ginx.Wrap(s.DstOnly))
+ server.POST("/full/start", ginx.Wrap(s.StartFullValidation))
+ server.POST("/full/stop", ginx.Wrap(s.StopFullValidation))
+ server.POST("/incr/stop", ginx.Wrap(s.StopIncrementValidation))
+ server.POST("/incr/start", ginx.WrapBodyV1[StartIncrRequest](s.StartIncrementValidation))
+}
+
+// ---- 下面是四个阶段 ---- //
+
+// SrcOnly 只读写源表
+func (s *Scheduler[T]) SrcOnly(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.pattern = connpool.PatternSrcOnly
+ s.pool.UpdatePattern(connpool.PatternSrcOnly)
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) SrcFirst(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.pattern = connpool.PatternSrcFirst
+ s.pool.UpdatePattern(connpool.PatternSrcFirst)
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) DstFirst(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.pattern = connpool.PatternDstFirst
+ s.pool.UpdatePattern(connpool.PatternDstFirst)
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) DstOnly(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.pattern = connpool.PatternDstOnly
+ s.pool.UpdatePattern(connpool.PatternDstOnly)
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) StopIncrementValidation(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.cancelIncr()
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) StartIncrementValidation(c *gin.Context,
+ req StartIncrRequest) (ginx.Result, error) {
+ // 开启增量校验
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ // 取消上一次的
+ cancel := s.cancelIncr
+ v, err := s.newValidator()
+ if err != nil {
+ return ginx.Result{
+ Code: 5,
+ Msg: "系统异常",
+ }, nil
+ }
+ v.Incr().Utime(req.Utime).
+ SleepInterval(time.Duration(req.Interval) * time.Millisecond)
+
+ go func() {
+ var ctx context.Context
+ ctx, s.cancelIncr = context.WithCancel(context.Background())
+ cancel()
+ err := v.Validate(ctx)
+ s.l.Warn("退出增量校验", logger.Error(err))
+ }()
+ return ginx.Result{
+ Msg: "启动增量校验成功",
+ }, nil
+}
+
+func (s *Scheduler[T]) StopFullValidation(c *gin.Context) (ginx.Result, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.cancelFull()
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+// StartFullValidation 全量校验
+func (s *Scheduler[T]) StartFullValidation(c *gin.Context) (ginx.Result, error) {
+ // 可以考虑去重的问题
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ // 取消上一次的
+ cancel := s.cancelFull
+ v, err := s.newValidator()
+ if err != nil {
+ return ginx.Result{}, err
+ }
+ var ctx context.Context
+ ctx, s.cancelFull = context.WithCancel(context.Background())
+
+ go func() {
+ // 先取消上一次的
+ cancel()
+ err := v.Validate(ctx)
+ if err != nil {
+ s.l.Warn("退出全量校验", logger.Error(err))
+ }
+ }()
+ return ginx.Result{
+ Msg: "OK",
+ }, nil
+}
+
+func (s *Scheduler[T]) newValidator() (*validator.Validator[T], error) {
+ switch s.pattern {
+ case connpool.PatternSrcOnly, connpool.PatternSrcFirst:
+ return validator.NewValidator[T](s.src, s.dst, "SRC", s.l, s.producer), nil
+ case connpool.PatternDstFirst, connpool.PatternDstOnly:
+ return validator.NewValidator[T](s.dst, s.src, "DST", s.l, s.producer), nil
+ default:
+ return nil, fmt.Errorf("未知的 pattern %s", s.pattern)
+ }
+}
+
+type StartIncrRequest struct {
+ Utime int64 `json:"utime"`
+ // 毫秒数
+ // json 不能正确处理 time.Duration 类型
+ Interval int64 `json:"interval"`
+}
diff --git a/webook/pkg/migrator/validator/base_validator.go b/webook/pkg/migrator/validator/base_validator.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1642e00d7b56eee171d4bedeff663cc216511dc
--- /dev/null
+++ b/webook/pkg/migrator/validator/base_validator.go
@@ -0,0 +1,39 @@
+package validator
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ events2 "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gorm.io/gorm"
+ "time"
+)
+
+type baseValidator struct {
+ base *gorm.DB
+ target *gorm.DB
+
+ // 这边需要告知,是以 SRC 为准,还是以 DST 为准
+ // 修复数据需要知道
+ direction string
+
+ l logger.LoggerV1
+ producer events2.Producer
+}
+
+// 上报不一致的数据
+func (v *baseValidator) notify(id int64, typ string) {
+ // 这里我们要单独控制超时时间
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ evt := events2.InconsistentEvent{
+ Direction: v.direction,
+ ID: id,
+ Type: typ,
+ }
+
+ err := v.producer.ProduceInconsistentEvent(ctx, evt)
+ if err != nil {
+ v.l.Error("发送消息失败", logger.Error(err),
+ logger.Field{Key: "event", Value: evt})
+ }
+}
diff --git a/webook/pkg/migrator/validator/canal.go b/webook/pkg/migrator/validator/canal.go
new file mode 100644
index 0000000000000000000000000000000000000000..33f22beb9dc6281c2c080762843785d4f970494d
--- /dev/null
+++ b/webook/pkg/migrator/validator/canal.go
@@ -0,0 +1,71 @@
+package validator
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ events2 "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "gorm.io/gorm"
+)
+
+type CanalIncrValidator[T migrator.Entity] struct {
+ baseValidator
+}
+
+func NewCanalIncrValidator[T migrator.Entity](
+ base *gorm.DB,
+ target *gorm.DB,
+ direction string,
+ l logger.LoggerV1,
+ producer events2.Producer,
+) *CanalIncrValidator[T] {
+ return &CanalIncrValidator[T]{
+ baseValidator: baseValidator{
+ base: base,
+ target: target,
+ direction: direction,
+ l: l,
+ producer: producer,
+ },
+ }
+}
+
+// Validate 一次校验一条
+func (v *CanalIncrValidator[T]) Validate(ctx context.Context, id int64) error {
+ var base T
+ err := v.base.WithContext(ctx).Where("id = ?", id).First(&base).Error
+ switch err {
+ case nil:
+ // 找到了
+ var target T
+ err1 := v.target.WithContext(ctx).Where("id = ?", id).First(&target).Error
+ switch err1 {
+ case nil:
+ // target 里面也找到了
+ if !base.CompareTo(target) {
+ v.notify(id, events2.InconsistentEventTypeNEQ)
+ }
+ case gorm.ErrRecordNotFound:
+ v.notify(id, events2.InconsistentEventTypeTargetMissing)
+ default:
+ return err
+ }
+ case gorm.ErrRecordNotFound:
+ // 找到了
+ var target T
+ err1 := v.target.WithContext(ctx).Where("id = ?", id).First(&target).Error
+ switch err1 {
+ case nil:
+ // target 里面也找到了
+ v.notify(id, events2.InconsistentEventTypeBaseMissing)
+ case gorm.ErrRecordNotFound:
+ // 两边都没了,啥也不需要干
+ default:
+ return err
+ }
+ // 收到消息的时候,或者说收到 binlog 的时候,这条数据已经没了
+ default:
+ return err
+ }
+ return nil
+}
diff --git a/webook/pkg/migrator/validator/validator.go b/webook/pkg/migrator/validator/validator.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2c3bafae172e2b0268d3236593db116f07afe6a
--- /dev/null
+++ b/webook/pkg/migrator/validator/validator.go
@@ -0,0 +1,183 @@
+package validator
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/migrator"
+ events2 "gitee.com/geekbang/basic-go/webook/pkg/migrator/events"
+ "github.com/ecodeclub/ekit/slice"
+ "golang.org/x/sync/errgroup"
+ "gorm.io/gorm"
+ "time"
+)
+
+type Validator[T migrator.Entity] struct {
+ baseValidator
+ batchSize int
+ utime int64
+ // 如果没有数据了,就睡眠
+ // 如果不是正数,那么就说明直接返回,结束这一次的循环
+ // 我很厌恶这种特殊值有特殊含义的做法,但是不得不搞
+ sleepInterval time.Duration
+}
+
+func NewValidator[T migrator.Entity](
+ base *gorm.DB,
+ target *gorm.DB,
+ direction string,
+ l logger.LoggerV1,
+ producer events2.Producer,
+) *Validator[T] {
+ return &Validator[T]{
+ baseValidator: baseValidator{
+ base: base,
+ target: target,
+ direction: direction,
+ l: l,
+ producer: producer,
+ },
+ batchSize: 100,
+ // 默认是全量校验,并且数据没了就结束
+ sleepInterval: 0,
+ }
+}
+
+func (v *Validator[T]) Utime(utime int64) *Validator[T] {
+ v.utime = utime
+ return v
+}
+
+func (v *Validator[T]) SleepInterval(i time.Duration) *Validator[T] {
+ v.sleepInterval = i
+ return v
+}
+
+// Validate 执行校验。
+// 分成两步:
+// 1. from => to
+func (v *Validator[T]) Validate(ctx context.Context) error {
+ var eg errgroup.Group
+ eg.Go(func() error {
+ return v.baseToTarget(ctx)
+ })
+ eg.Go(func() error {
+ return v.targetToBase(ctx)
+ })
+ return eg.Wait()
+}
+
+// baseToTarget 从 first 到 second 的验证
+func (v *Validator[T]) baseToTarget(ctx context.Context) error {
+ offset := 0
+ for {
+ var src T
+ // 这里假定主键的规范都是叫做 id,基本上大部分公司都有这种规范
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ err := v.base.WithContext(dbCtx).
+ Order("id").
+ Where("utime >= ?", v.utime).
+ Offset(offset).First(&src).Error
+ cancel()
+ switch err {
+ case gorm.ErrRecordNotFound:
+ // 已经没有数据了
+ if v.sleepInterval <= 0 {
+ return nil
+ }
+ time.Sleep(v.sleepInterval)
+ continue
+ case context.Canceled, context.DeadlineExceeded:
+ // 退出循环
+ return nil
+ case nil:
+ v.dstDiff(ctx, src)
+ default:
+ v.l.Error("src => dst 查询源表失败", logger.Error(err))
+ }
+ offset++
+ }
+}
+
+func (v *Validator[T]) dstDiff(ctx context.Context, src T) {
+ var dst T
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ err := v.target.WithContext(dbCtx).
+ Where("id=?", src.ID()).First(&dst).Error
+ cancel()
+ // 这边要考虑不同的 error
+ switch err {
+ case gorm.ErrRecordNotFound:
+ v.notify(src.ID(), events2.InconsistentEventTypeTargetMissing)
+ case nil:
+ // 查询到了数据
+ equal := src.CompareTo(dst)
+ if !equal {
+ v.notify(src.ID(), events2.InconsistentEventTypeNEQ)
+ }
+ default:
+ v.l.Error("src => dst 查询目标表失败", logger.Error(err))
+ }
+}
+
+// targetToBase 反过来,执行 target 到 base 的验证
+// 这是为了找出 dst 中多余的数据
+func (v *Validator[T]) targetToBase(ctx context.Context) error {
+ // 这个我们只需要找出 src 中不存在的 id 就可以了
+ offset := 0
+ for {
+ var ts []T
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ err := v.target.WithContext(dbCtx).Model(new(T)).Select("id").Offset(offset).
+ Limit(v.batchSize).Find(&ts).Error
+ cancel()
+ switch err {
+ case gorm.ErrRecordNotFound:
+ if v.sleepInterval > 0 {
+ time.Sleep(v.sleepInterval)
+ // 在 sleep 的时候。不需要调整偏移量
+ continue
+ }
+ case context.DeadlineExceeded, context.Canceled:
+ return nil
+ case nil:
+ v.srcMissingRecords(ctx, ts)
+ default:
+ v.l.Error("dst => src 查询目标表失败", logger.Error(err))
+ }
+ if len(ts) < v.batchSize {
+ // 数据没了
+ return nil
+ }
+ offset += v.batchSize
+ }
+}
+
+func (v *Validator[T]) srcMissingRecords(ctx context.Context, ts []T) {
+ ids := slice.Map(ts, func(idx int, src T) int64 {
+ return src.ID()
+ })
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ base := v.base.WithContext(dbCtx)
+ var srcTs []T
+ err := base.Select("id").Where("id IN ?", ids).Find(&srcTs).Error
+ switch err {
+ case gorm.ErrRecordNotFound:
+ // 说明 ids 全部没有
+ v.notifySrcMissing(ts)
+ case nil:
+ // 计算差集
+ missing := slice.DiffSetFunc(ts, srcTs, func(src, dst T) bool {
+ return src.ID() == dst.ID()
+ })
+ v.notifySrcMissing(missing)
+ default:
+ v.l.Error("dst => src 查询源表失败", logger.Error(err))
+ }
+}
+
+func (v *Validator[T]) notifySrcMissing(ts []T) {
+ for _, t := range ts {
+ v.notify(t.ID(), events2.InconsistentEventTypeBaseMissing)
+ }
+}
diff --git a/webook/pkg/netx/ip.go b/webook/pkg/netx/ip.go
new file mode 100644
index 0000000000000000000000000000000000000000..8ebda4269b3078771b15af8eef83dcc68ab98141
--- /dev/null
+++ b/webook/pkg/netx/ip.go
@@ -0,0 +1,16 @@
+package netx
+
+import "net"
+
+// GetOutboundIP 获得对外发送消息的 IP 地址
+func GetOutboundIP() string {
+ // DNS 的地址,国内可以用 114.114.114.114
+ conn, err := net.Dial("udp", "8.8.8.8:80")
+ if err != nil {
+ return ""
+ }
+ defer conn.Close()
+
+ localAddr := conn.LocalAddr().(*net.UDPAddr)
+ return localAddr.IP.String()
+}
diff --git a/webook/pkg/ratelimit/mocks/ratelimit.mock.go b/webook/pkg/ratelimit/mocks/ratelimit.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..6cd563e525db0fec9305444e81d6bdaec8949060
--- /dev/null
+++ b/webook/pkg/ratelimit/mocks/ratelimit.mock.go
@@ -0,0 +1,50 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: webook/pkg/ratelimit/types.go
+
+// Package limitmocks is a generated GoMock package.
+package limitmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockLimiter is a mock of Limiter interface.
+type MockLimiter struct {
+ ctrl *gomock.Controller
+ recorder *MockLimiterMockRecorder
+}
+
+// MockLimiterMockRecorder is the mock recorder for MockLimiter.
+type MockLimiterMockRecorder struct {
+ mock *MockLimiter
+}
+
+// NewMockLimiter creates a new mock instance.
+func NewMockLimiter(ctrl *gomock.Controller) *MockLimiter {
+ mock := &MockLimiter{ctrl: ctrl}
+ mock.recorder = &MockLimiterMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLimiter) EXPECT() *MockLimiterMockRecorder {
+ return m.recorder
+}
+
+// Limit mocks base method.
+func (m *MockLimiter) Limit(ctx context.Context, key string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Limit", ctx, key)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Limit indicates an expected call of Limit.
+func (mr *MockLimiterMockRecorder) Limit(ctx, key interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Limit", reflect.TypeOf((*MockLimiter)(nil).Limit), ctx, key)
+}
diff --git a/webook/pkg/ratelimit/redis_slide_window.go b/webook/pkg/ratelimit/redis_slide_window.go
new file mode 100644
index 0000000000000000000000000000000000000000..bef764db432d61f70b484c383c17deeff4ceb63f
--- /dev/null
+++ b/webook/pkg/ratelimit/redis_slide_window.go
@@ -0,0 +1,37 @@
+package ratelimit
+
+import (
+ "context"
+ _ "embed"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+//go:embed slide_window.lua
+var luaSlideWindow string
+
+// RedisSlidingWindowLimiter Redis 上的滑动窗口算法限流器实现
+type RedisSlidingWindowLimiter struct {
+ cmd redis.Cmdable
+
+ // 窗口大小
+ interval time.Duration
+ // 阈值
+ rate int
+ // interval 内允许 rate 个请求
+ // 1s 内允许 3000 个请求
+}
+
+func NewRedisSlidingWindowLimiter(cmd redis.Cmdable,
+ interval time.Duration, rate int) Limiter {
+ return &RedisSlidingWindowLimiter{
+ cmd: cmd,
+ interval: interval,
+ rate: rate,
+ }
+}
+
+func (r *RedisSlidingWindowLimiter) Limit(ctx context.Context, key string) (bool, error) {
+ return r.cmd.Eval(ctx, luaSlideWindow, []string{key},
+ r.interval.Milliseconds(), r.rate, time.Now().UnixMilli()).Bool()
+}
diff --git a/webook/pkg/ratelimit/slide_window.lua b/webook/pkg/ratelimit/slide_window.lua
new file mode 100644
index 0000000000000000000000000000000000000000..ee058b05dfea0cbb17d1807e962d02246d16a406
--- /dev/null
+++ b/webook/pkg/ratelimit/slide_window.lua
@@ -0,0 +1,26 @@
+-- 1, 2, 3, 4, 5, 6, 7 这是你的元素
+-- ZREMRANGEBYSCORE key1 0 6
+-- 7 执行完之后
+
+-- 限流对象
+local key = KEYS[1]
+-- 窗口大小
+local window = tonumber(ARGV[1])
+-- 阈值
+local threshold = tonumber( ARGV[2])
+local now = tonumber(ARGV[3])
+-- 窗口的起始时间
+local min = now - window
+
+redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
+local cnt = redis.call('ZCOUNT', key, '-inf', '+inf')
+-- local cnt = redis.call('ZCOUNT', key, min, '+inf')
+if cnt >= threshold then
+ -- 执行限流
+ return "true"
+else
+ -- 把 score 和 member 都设置成 now
+ redis.call('ZADD', key, now, now)
+ redis.call('PEXPIRE', key, window)
+ return "false"
+end
\ No newline at end of file
diff --git a/webook/pkg/ratelimit/types.go b/webook/pkg/ratelimit/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..7e3e9344b164d0069bd5e86512c93d6810fd51f6
--- /dev/null
+++ b/webook/pkg/ratelimit/types.go
@@ -0,0 +1,10 @@
+package ratelimit
+
+import "context"
+
+type Limiter interface {
+ // Limit 有咩有触发限流。key 就是限流对象
+ // bool 代表是否限流,true 就是要限流
+ // err 限流器本身有咩有错误
+ Limit(ctx context.Context, key string) (bool, error)
+}
diff --git a/webook/pkg/redisx/prometheus.go b/webook/pkg/redisx/prometheus.go
new file mode 100644
index 0000000000000000000000000000000000000000..e8f4e0c04748fcb4380ec45e284385cf0d44a92b
--- /dev/null
+++ b/webook/pkg/redisx/prometheus.go
@@ -0,0 +1,62 @@
+package redisx
+
+import (
+ "context"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/redis/go-redis/v9"
+ "net"
+ "strconv"
+ "time"
+)
+
+type PrometheusHook struct {
+ vector *prometheus.SummaryVec
+}
+
+func NewPrometheusHook(opt prometheus.SummaryOpts) *PrometheusHook {
+ vector := prometheus.NewSummaryVec(opt,
+ // key_exist 是否命中缓存
+ []string{"cmd", "key_exist"})
+ prometheus.MustRegister(vector)
+ return &PrometheusHook{
+ vector: vector,
+ }
+}
+
+func (p *PrometheusHook) DialHook(next redis.DialHook) redis.DialHook {
+ return func(ctx context.Context, network, addr string) (net.Conn, error) {
+ // 相当于,你这里啥也不干
+ return next(ctx, network, addr)
+ }
+}
+
+func (p *PrometheusHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
+ return func(ctx context.Context, cmd redis.Cmder) error {
+ // 在Redis执行之前
+ startTime := time.Now()
+ var err error
+ defer func() {
+ duration := time.Since(startTime).Milliseconds()
+ //biz := ctx.Value("biz")
+ keyExist := err == redis.Nil
+ p.vector.WithLabelValues(
+ cmd.Name(),
+
+ strconv.FormatBool(keyExist),
+ ).Observe(float64(duration))
+ }()
+ // 这个会最终发送命令到 redis 上
+ err = next(ctx, cmd)
+ // 在 Redis 执行之后
+ return err
+ }
+}
+
+func (p *PrometheusHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
+ //TODO implement me
+ panic("implement me")
+}
+
+//func Use(client *redis.Client) {
+// client.AddHook()
+//}
diff --git a/webook/pkg/saramax/batch_consumer_handler.go b/webook/pkg/saramax/batch_consumer_handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..23dac91419bb4f767f60b5b8b1922c16e07a118c
--- /dev/null
+++ b/webook/pkg/saramax/batch_consumer_handler.go
@@ -0,0 +1,81 @@
+package saramax
+
+import (
+ "context"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+type BatchHandler[T any] struct {
+ l logger.LoggerV1
+ fn func(msgs []*sarama.ConsumerMessage, ts []T) error
+ // 用 option 模式来设置这个 batchSize 和 duration
+ batchSize int
+ batchDuration time.Duration
+}
+
+func NewBatchHandler[T any](l logger.LoggerV1, fn func(msgs []*sarama.ConsumerMessage, ts []T) error) *BatchHandler[T] {
+ return &BatchHandler[T]{l: l, fn: fn, batchDuration: time.Second, batchSize: 10}
+}
+
+func (b *BatchHandler[T]) Setup(session sarama.ConsumerGroupSession) error {
+ return nil
+}
+
+func (b *BatchHandler[T]) Cleanup(session sarama.ConsumerGroupSession) error {
+ return nil
+}
+
+func (b *BatchHandler[T]) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
+ msgsCh := claim.Messages()
+ batchSize := b.batchSize
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), b.batchDuration)
+ done := false
+ msgs := make([]*sarama.ConsumerMessage, 0, batchSize)
+ ts := make([]T, 0, batchSize)
+ for i := 0; i < batchSize && !done; i++ {
+ select {
+ case <-ctx.Done():
+ done = true
+ case msg, ok := <-msgsCh:
+ // 再按照 key 或者业务 ID 转发到不同的 channel
+ if !ok {
+ cancel()
+ // 代表消费者被关闭了
+ return nil
+ }
+ var t T
+ err := json.Unmarshal(msg.Value, &t)
+ if err != nil {
+ b.l.Error("反序列化失败",
+ logger.Error(err),
+ logger.String("topic", msg.Topic),
+ logger.Int64("partition", int64(msg.Partition)),
+ logger.Int64("offset", msg.Offset))
+ continue
+ }
+ msgs = append(msgs, msg)
+ ts = append(ts, t)
+ }
+ }
+ cancel()
+ if len(msgs) == 0 {
+ continue
+ }
+ err := b.fn(msgs, ts)
+ if err != nil {
+ b.l.Error("调用业务批量接口失败",
+ logger.Error(err))
+ // 你这里整个批次都要记下来
+
+ // 还要继续往前消费
+ }
+ for _, msg := range msgs {
+ // 这样,万无一失
+ session.MarkMessage(msg, "")
+ }
+ }
+}
diff --git a/webook/pkg/saramax/consumer_handler.go b/webook/pkg/saramax/consumer_handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..3a92261cb1dc4016d0c698ced4c63723590b99ab
--- /dev/null
+++ b/webook/pkg/saramax/consumer_handler.go
@@ -0,0 +1,68 @@
+package saramax
+
+import (
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/IBM/sarama"
+)
+
+type HandlerV1[T any] func(msg *sarama.ConsumerMessage, t T) error
+
+type Handler[T any] struct {
+ l logger.LoggerV1
+ fn func(msg *sarama.ConsumerMessage, t T) error
+}
+
+func NewHandler[T any](l logger.LoggerV1, fn func(msg *sarama.ConsumerMessage, t T) error) *Handler[T] {
+ return &Handler[T]{
+ l: l,
+ fn: fn,
+ }
+}
+
+func (h Handler[T]) Setup(session sarama.ConsumerGroupSession) error {
+ return nil
+}
+
+func (h Handler[T]) Cleanup(session sarama.ConsumerGroupSession) error {
+ return nil
+}
+
+func (h Handler[T]) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
+ msgs := claim.Messages()
+ for msg := range msgs {
+ var t T
+ err := json.Unmarshal(msg.Value, &t)
+ if err != nil {
+ h.l.Error("反序列化消息失败",
+ logger.Error(err),
+ logger.String("topic", msg.Topic),
+ logger.Int64("partition", int64(msg.Partition)),
+ logger.Int64("offset", msg.Offset))
+ continue
+ }
+ // 在这里执行重试
+ for i := 0; i < 3; i++ {
+ err = h.fn(msg, t)
+ if err == nil {
+ break
+ }
+ h.l.Error("处理消息失败",
+ logger.Error(err),
+ logger.String("topic", msg.Topic),
+ logger.Int64("partition", int64(msg.Partition)),
+ logger.Int64("offset", msg.Offset))
+ }
+
+ if err != nil {
+ h.l.Error("处理消息失败-重试次数上限",
+ logger.Error(err),
+ logger.String("topic", msg.Topic),
+ logger.Int64("partition", int64(msg.Partition)),
+ logger.Int64("offset", msg.Offset))
+ } else {
+ session.MarkMessage(msg, "")
+ }
+ }
+ return nil
+}
diff --git a/webook/pkg/saramax/types.go b/webook/pkg/saramax/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..c2e9d8cb6487d9ba763c11699cac4c6696f82253
--- /dev/null
+++ b/webook/pkg/saramax/types.go
@@ -0,0 +1,5 @@
+package saramax
+
+type Consumer interface {
+ Start() error
+}
diff --git a/webook/pkg/wego/app.go b/webook/pkg/wego/app.go
new file mode 100644
index 0000000000000000000000000000000000000000..39d848fb3dd9f3a79c7b6ff9dd34d3b18e3363b9
--- /dev/null
+++ b/webook/pkg/wego/app.go
@@ -0,0 +1,16 @@
+// Package wego 是一个随便取的名字
+package wego
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/ginx"
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+)
+
+// App 当你在 wire 里面使用这个结构体的时候,要注意不是所有的服务都需要全部字段,
+// 那么在 wire 的时候就不要使用 * 了
+type App struct {
+ GRPCServer *grpcx.Server
+ WebServer *ginx.Server
+ Consumers []saramax.Consumer
+}
diff --git a/webook/pkg/zapx/sensitive_log.go b/webook/pkg/zapx/sensitive_log.go
new file mode 100644
index 0000000000000000000000000000000000000000..d34d96fb18dd0091721e755b92f15898d5fe7f49
--- /dev/null
+++ b/webook/pkg/zapx/sensitive_log.go
@@ -0,0 +1,28 @@
+package zapx
+
+import (
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+type MyCore struct {
+ zapcore.Core
+}
+
+func (c MyCore) Write(entry zapcore.Entry, fds []zapcore.Field) error {
+ for _, fd := range fds {
+ if fd.Key == "phone" {
+ phone := fd.String
+ fd.String = phone[:3] + "****" + phone[7:]
+ }
+ }
+ return c.Core.Write(entry, fds)
+}
+
+func MaskPhone(key string, value string) zap.Field {
+ value = value[:3] + "****" + value[7:]
+ return zap.Field{
+ Key: key,
+ String: value,
+ }
+}
diff --git a/webook/prometheus-example.yaml b/webook/prometheus-example.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d0f2bb81c2335adec7851486d0a762ae342bac15
--- /dev/null
+++ b/webook/prometheus-example.yaml
@@ -0,0 +1,427 @@
+# 这是一个比较完整的配置,但是因为有很多配置项,
+# 所以我不建议你一开始的时候就来学习这个
+# 我建议你在后面有需要的时候再来看
+global:
+ scrape_interval: 15s
+ evaluation_interval: 30s
+ body_size_limit: 15MB
+ sample_limit: 1500
+ target_limit: 30
+ label_limit: 30
+ label_name_length_limit: 200
+ label_value_length_limit: 200
+ # scrape_timeout is set to the global default (10s).
+
+ external_labels:
+ monitor: codelab
+ foo: bar
+
+rule_files:
+ - "first.rules"
+ - "my/*.rules"
+
+remote_write:
+ - url: http://remote1/push
+ name: drop_expensive
+ write_relabel_configs:
+ - source_labels: [__name__]
+ regex: expensive.*
+ action: drop
+ oauth2:
+ client_id: "123"
+ client_secret: "456"
+ token_url: "http://remote1/auth"
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ - url: http://remote2/push
+ name: rw_tls
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+ headers:
+ name: value
+
+remote_read:
+ - url: http://remote1/read
+ read_recent: true
+ name: default
+ enable_http2: false
+ - url: http://remote3/read
+ read_recent: false
+ name: read_special
+ required_matchers:
+ job: special
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+scrape_configs:
+ - job_name: prometheus
+
+ honor_labels: true
+ # scrape_interval is defined by the configured global (15s).
+ # scrape_timeout is defined by the global default (10s).
+
+ # metrics_path defaults to '/metrics'
+ # scheme defaults to 'http'.
+
+ file_sd_configs:
+ - files:
+ - foo/*.slow.json
+ - foo/*.slow.yml
+ - single/file.yml
+ refresh_interval: 10m
+ - files:
+ - bar/*.yaml
+
+ static_configs:
+ - targets: ["localhost:9090", "localhost:9191"]
+ labels:
+ my: label
+ your: label
+
+ relabel_configs:
+ - source_labels: [job, __meta_dns_name]
+ regex: (.*)some-[regex]
+ target_label: job
+ replacement: foo-${1}
+ # action defaults to 'replace'
+ - source_labels: [abc]
+ target_label: cde
+ - replacement: static
+ target_label: abc
+ - regex:
+ replacement: static
+ target_label: abc
+ - source_labels: [foo]
+ target_label: abc
+ action: keepequal
+ - source_labels: [foo]
+ target_label: abc
+ action: dropequal
+
+ authorization:
+ credentials_file: valid_token_file
+
+ tls_config:
+ min_version: TLS10
+
+ - job_name: service-x
+
+ basic_auth:
+ username: admin_name
+ password: "multiline\nmysecret\ntest"
+
+ scrape_interval: 50s
+ scrape_timeout: 5s
+
+ body_size_limit: 10MB
+ sample_limit: 1000
+ target_limit: 35
+ label_limit: 35
+ label_name_length_limit: 210
+ label_value_length_limit: 210
+
+
+ metrics_path: /my_path
+ scheme: https
+
+ dns_sd_configs:
+ - refresh_interval: 15s
+ names:
+ - first.dns.address.domain.com
+ - second.dns.address.domain.com
+ - names:
+ - first.dns.address.domain.com
+
+ relabel_configs:
+ - source_labels: [job]
+ regex: (.*)some-[regex]
+ action: drop
+ - source_labels: [__address__]
+ modulus: 8
+ target_label: __tmp_hash
+ action: hashmod
+ - source_labels: [__tmp_hash]
+ regex: 1
+ action: keep
+ - action: labelmap
+ regex: 1
+ - action: labeldrop
+ regex: d
+ - action: labelkeep
+ regex: k
+
+ metric_relabel_configs:
+ - source_labels: [__name__]
+ regex: expensive_metric.*
+ action: drop
+
+ - job_name: service-y
+
+ consul_sd_configs:
+ - server: "localhost:1234"
+ token: mysecret
+ path_prefix: /consul
+ services: ["nginx", "cache", "mysql"]
+ tags: ["canary", "v1"]
+ node_meta:
+ rack: "123"
+ allow_stale: true
+ scheme: https
+ tls_config:
+ ca_file: valid_ca_file
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+ insecure_skip_verify: false
+
+ relabel_configs:
+ - source_labels: [__meta_sd_consul_tags]
+ separator: ","
+ regex: label:([^=]+)=([^,]+)
+ target_label: ${1}
+ replacement: ${2}
+
+ - job_name: service-z
+
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ authorization:
+ credentials: mysecret
+
+ - job_name: service-kubernetes
+
+ kubernetes_sd_configs:
+ - role: endpoints
+ api_server: "https://localhost:1234"
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ basic_auth:
+ username: "myusername"
+ password: "mysecret"
+
+ - job_name: service-kubernetes-namespaces
+
+ kubernetes_sd_configs:
+ - role: endpoints
+ api_server: "https://localhost:1234"
+ namespaces:
+ names:
+ - default
+
+ basic_auth:
+ username: "myusername"
+ password_file: valid_password_file
+
+ - job_name: service-kuma
+
+ kuma_sd_configs:
+ - server: http://kuma-control-plane.kuma-system.svc:5676
+
+ - job_name: service-marathon
+ marathon_sd_configs:
+ - servers:
+ - "https://marathon.example.com:443"
+
+ auth_token: "mysecret"
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ - job_name: service-nomad
+ nomad_sd_configs:
+ - server: 'http://localhost:4646'
+
+ - job_name: service-ec2
+ ec2_sd_configs:
+ - region: us-east-1
+ access_key: access
+ secret_key: mysecret
+ profile: profile
+ filters:
+ - name: tag:environment
+ values:
+ - prod
+
+ - name: tag:service
+ values:
+ - web
+ - db
+
+ - job_name: service-lightsail
+ lightsail_sd_configs:
+ - region: us-east-1
+ access_key: access
+ secret_key: mysecret
+ profile: profile
+
+ - job_name: service-azure
+ azure_sd_configs:
+ - environment: AzurePublicCloud
+ authentication_method: OAuth
+ subscription_id: 11AAAA11-A11A-111A-A111-1111A1111A11
+ resource_group: my-resource-group
+ tenant_id: BBBB222B-B2B2-2B22-B222-2BB2222BB2B2
+ client_id: 333333CC-3C33-3333-CCC3-33C3CCCCC33C
+ client_secret: mysecret
+ port: 9100
+
+ - job_name: service-nerve
+ nerve_sd_configs:
+ - servers:
+ - localhost
+ paths:
+ - /monitoring
+
+ - job_name: 0123service-xxx
+ metrics_path: /metrics
+ static_configs:
+ - targets:
+ - localhost:9090
+
+ - job_name: badfederation
+ honor_timestamps: false
+ metrics_path: /federate
+ static_configs:
+ - targets:
+ - localhost:9090
+
+ - job_name: 測試
+ metrics_path: /metrics
+ static_configs:
+ - targets:
+ - localhost:9090
+
+ - job_name: httpsd
+ http_sd_configs:
+ - url: "http://example.com/prometheus"
+
+ - job_name: service-triton
+ triton_sd_configs:
+ - account: "testAccount"
+ dns_suffix: "triton.example.com"
+ endpoint: "triton.example.com"
+ port: 9163
+ refresh_interval: 1m
+ version: 1
+ tls_config:
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ - job_name: digitalocean-droplets
+ digitalocean_sd_configs:
+ - authorization:
+ credentials: abcdef
+
+ - job_name: docker
+ docker_sd_configs:
+ - host: unix:///var/run/docker.sock
+
+ - job_name: dockerswarm
+ dockerswarm_sd_configs:
+ - host: http://127.0.0.1:2375
+ role: nodes
+
+ - job_name: service-openstack
+ openstack_sd_configs:
+ - role: instance
+ region: RegionOne
+ port: 80
+ refresh_interval: 1m
+ tls_config:
+ ca_file: valid_ca_file
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ - job_name: service-puppetdb
+ puppetdb_sd_configs:
+ - url: https://puppetserver/
+ query: 'resources { type = "Package" and title = "httpd" }'
+ include_parameters: true
+ port: 80
+ refresh_interval: 1m
+ tls_config:
+ ca_file: valid_ca_file
+ cert_file: valid_cert_file
+ key_file: valid_key_file
+
+ - job_name: hetzner
+ relabel_configs:
+ - action: uppercase
+ source_labels: [instance]
+ target_label: instance
+ hetzner_sd_configs:
+ - role: hcloud
+ authorization:
+ credentials: abcdef
+ - role: robot
+ basic_auth:
+ username: abcdef
+ password: abcdef
+
+ - job_name: service-eureka
+ eureka_sd_configs:
+ - server: "http://eureka.example.com:8761/eureka"
+
+ - job_name: ovhcloud
+ ovhcloud_sd_configs:
+ - service: vps
+ endpoint: ovh-eu
+ application_key: testAppKey
+ application_secret: testAppSecret
+ consumer_key: testConsumerKey
+ refresh_interval: 1m
+ - service: dedicated_server
+ endpoint: ovh-eu
+ application_key: testAppKey
+ application_secret: testAppSecret
+ consumer_key: testConsumerKey
+ refresh_interval: 1m
+
+ - job_name: scaleway
+ scaleway_sd_configs:
+ - role: instance
+ project_id: 11111111-1111-1111-1111-111111111112
+ access_key: SCWXXXXXXXXXXXXXXXXX
+ secret_key: 11111111-1111-1111-1111-111111111111
+ - role: baremetal
+ project_id: 11111111-1111-1111-1111-111111111112
+ access_key: SCWXXXXXXXXXXXXXXXXX
+ secret_key: 11111111-1111-1111-1111-111111111111
+
+ - job_name: linode-instances
+ linode_sd_configs:
+ - authorization:
+ credentials: abcdef
+
+ - job_name: uyuni
+ uyuni_sd_configs:
+ - server: https://localhost:1234
+ username: gopher
+ password: hole
+
+ - job_name: ionos
+ ionos_sd_configs:
+ - datacenter_id: 8feda53f-15f0-447f-badf-ebe32dad2fc0
+ authorization:
+ credentials: abcdef
+
+ - job_name: vultr
+ vultr_sd_configs:
+ - authorization:
+ credentials: abcdef
+
+alerting:
+ alertmanagers:
+ - scheme: https
+ static_configs:
+ - targets:
+ - "1.2.3.4:9093"
+ - "1.2.3.5:9093"
+ - "1.2.3.6:9093"
diff --git a/webook/prometheus.yaml b/webook/prometheus.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d82ed4c2e8763ceb6cfe7001390a225aa09fa6f8
--- /dev/null
+++ b/webook/prometheus.yaml
@@ -0,0 +1,7 @@
+scrape_configs:
+ - job_name: "webook"
+ scrape_interval: 5s
+ scrape_timeout: 3s
+ static_configs:
+# - 这个是访问我 webook 上的采集数据的端口
+ - targets: ["host.docker.internal:8081"]
\ No newline at end of file
diff --git a/webook/reward/config/dev.yaml b/webook/reward/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9d0526b5512a6a33402db69c62c31f2efc181a5a
--- /dev/null
+++ b/webook/reward/config/dev.yaml
@@ -0,0 +1,16 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook_reward"
+
+grpc:
+ server:
+ port: 8099
+ etcdTTL: 60
+ client:
+ payment:
+ target: "etcd:///service/payment"
+ account:
+ target: "etcd:///service/account"
+
+etcd:
+ endpoints:
+ - "localhost:12379"
\ No newline at end of file
diff --git a/webook/reward/domain/biz.go b/webook/reward/domain/biz.go
new file mode 100644
index 0000000000000000000000000000000000000000..4188b5afd9dbfed0474792aa9373d53fba102c1e
--- /dev/null
+++ b/webook/reward/domain/biz.go
@@ -0,0 +1 @@
+package domain
diff --git a/webook/reward/domain/reward.go b/webook/reward/domain/reward.go
new file mode 100644
index 0000000000000000000000000000000000000000..47709ad424e8c21e834d39cc3d2961e0bb798932
--- /dev/null
+++ b/webook/reward/domain/reward.go
@@ -0,0 +1,47 @@
+package domain
+
+type Target struct {
+ // 因为什么而打赏
+ Biz string
+ BizId int64
+ // 作为一个可选的东西
+ // 也就是你要打赏的东西是什么
+ BizName string
+
+ // 打赏的目标用户
+ Uid int64
+}
+
+type Reward struct {
+ Id int64
+ Uid int64
+ Target Target
+ // 同样不着急引入货币。
+ Amt int64
+ Status RewardStatus
+}
+
+// Completed 是否已经完成
+// 目前来说,也就是是否处理了支付回调
+func (r Reward) Completed() bool {
+ return r.Status == RewardStatusFailed || r.Status == RewardStatusPayed
+}
+
+type RewardStatus uint8
+
+func (r RewardStatus) AsUint8() uint8 {
+ return uint8(r)
+}
+
+const (
+ RewardStatusUnknown = iota
+ RewardStatusInit
+ RewardStatusPayed
+ RewardStatusFailed
+)
+
+// 垃圾设计
+type CodeURL struct {
+ Rid int64
+ URL string
+}
diff --git a/webook/reward/events/consumer.go b/webook/reward/events/consumer.go
new file mode 100644
index 0000000000000000000000000000000000000000..1f8fae240fb957b71c99838d13d81111187b5a85
--- /dev/null
+++ b/webook/reward/events/consumer.go
@@ -0,0 +1,73 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "github.com/IBM/sarama"
+ "strings"
+ "time"
+)
+
+type PaymentEvent struct {
+ // biz string
+ BizTradeNO string
+ Status uint8
+}
+
+func (p PaymentEvent) ToDomainStatus() domain.RewardStatus {
+ // PaymentStatusInit
+ // PaymentStatusSuccess
+ // PaymentStatusFailed
+ // PaymentStatusRefund
+ switch p.Status {
+ // 这里不能引用 payment 里面的定义,只能手写
+ case 1:
+ return domain.RewardStatusInit
+ case 2:
+ return domain.RewardStatusPayed
+ case 3, 4:
+ return domain.RewardStatusFailed
+ default:
+ return domain.RewardStatusUnknown
+ }
+}
+
+type PaymentEventConsumer struct {
+ client sarama.Client
+ l logger.LoggerV1
+ svc service.RewardService
+}
+
+// Start 这边就是自己启动 goroutine 了
+func (r *PaymentEventConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("reward",
+ r.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{"payment_events"},
+ saramax.NewHandler[PaymentEvent](r.l, r.Consume))
+ if err != nil {
+ r.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (r *PaymentEventConsumer) Consume(
+ msg *sarama.ConsumerMessage,
+ evt PaymentEvent) error {
+ // 不是我们的,我只处理 reward
+ // biz_trade_no 是以 reward 为开头的
+ if !strings.HasPrefix(evt.BizTradeNO, "reward") {
+ return nil
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ return r.svc.UpdateReward(ctx, evt.BizTradeNO, evt.ToDomainStatus())
+}
diff --git a/webook/reward/grpc/reward.go b/webook/reward/grpc/reward.go
new file mode 100644
index 0000000000000000000000000000000000000000..3098349c2279d1d2a972831366979dc3551e61fa
--- /dev/null
+++ b/webook/reward/grpc/reward.go
@@ -0,0 +1,51 @@
+package grpc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/api/proto/gen/reward/v1"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "google.golang.org/grpc"
+)
+
+type RewardServiceServer struct {
+ rewardv1.UnimplementedRewardServiceServer
+ svc service.RewardService
+}
+
+func NewRewardServiceServer(svc service.RewardService) *RewardServiceServer {
+ return &RewardServiceServer{svc: svc}
+}
+
+func (r *RewardServiceServer) Register(server *grpc.Server) {
+ rewardv1.RegisterRewardServiceServer(server, r)
+}
+
+func (r *RewardServiceServer) PreReward(ctx context.Context, request *rewardv1.PreRewardRequest) (*rewardv1.PreRewardResponse, error) {
+ codeURL, err := r.svc.PreReward(ctx, domain.Reward{
+ Uid: request.Uid,
+ Target: domain.Target{
+ Biz: request.Biz,
+ BizId: request.BizId,
+ BizName: request.BizName,
+ Uid: request.Uid,
+ },
+ Amt: request.Amt,
+ })
+ return &rewardv1.PreRewardResponse{
+ CodeUrl: codeURL.URL,
+ Rid: codeURL.Rid,
+ }, err
+}
+
+func (r *RewardServiceServer) GetReward(ctx context.Context,
+ req *rewardv1.GetRewardRequest) (*rewardv1.GetRewardResponse, error) {
+ rw, err := r.svc.GetReward(ctx, req.GetRid(), req.GetUid())
+ if err != nil {
+ return nil, err
+ }
+ return &rewardv1.GetRewardResponse{
+ // 两个的取值是一样的,所以可以直接转
+ Status: rewardv1.RewardStatus(rw.Status),
+ }, nil
+}
diff --git a/webook/reward/integration/startup/db.go b/webook/reward/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..6c32665786bccc45e3f231e2c6a0b385b6407e87
--- /dev/null
+++ b/webook/reward/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook_reward"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/reward/integration/startup/wire.go b/webook/reward/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..444e9f72c0540a581b48f95087a63fa1715fceb8
--- /dev/null
+++ b/webook/reward/integration/startup/wire.go
@@ -0,0 +1,22 @@
+//go:build wireinject
+
+package startup
+
+import (
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ "gitee.com/geekbang/basic-go/webook/reward/repository"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "github.com/google/wire"
+)
+
+var thirdPartySet = wire.NewSet(InitTestDB, InitLogger, InitRedis)
+
+func InitWechatNativeSvc(client pmtv1.WechatPaymentServiceClient) *service.WechatNativeRewardService {
+ wire.Build(service.NewWechatNativeRewardService,
+ thirdPartySet,
+ cache.NewRewardRedisCache,
+ repository.NewRewardRepository, dao.NewRewardGORMDAO)
+ return new(service.WechatNativeRewardService)
+}
diff --git a/webook/reward/integration/startup/wire_gen.go b/webook/reward/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..08f656b9a021ed9a398ba2e4d82a690934c1bbca
--- /dev/null
+++ b/webook/reward/integration/startup/wire_gen.go
@@ -0,0 +1,33 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ "gitee.com/geekbang/basic-go/webook/reward/repository"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitWechatNativeSvc(client pmtv1.WechatPaymentServiceClient) *service.WechatNativeRewardService {
+ gormDB := InitTestDB()
+ rewardDAO := dao.NewRewardGORMDAO(gormDB)
+ cmdable := InitRedis()
+ rewardCache := cache.NewRewardRedisCache(cmdable)
+ rewardRepository := repository.NewRewardRepository(rewardDAO, rewardCache)
+ loggerV1 := InitLogger()
+ wechatNativeRewardService := service.NewWechatNativeRewardService(client, rewardRepository, loggerV1)
+ return wechatNativeRewardService
+}
+
+// wire.go:
+
+var thirdPartySet = wire.NewSet(InitTestDB, InitLogger, InitRedis)
diff --git a/webook/reward/integration/wechat_native_service_test.go b/webook/reward/integration/wechat_native_service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd1673b3acec6667a4a146d9d31d07820c2271a4
--- /dev/null
+++ b/webook/reward/integration/wechat_native_service_test.go
@@ -0,0 +1,155 @@
+package integration
+
+import (
+ "context"
+ "fmt"
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ pmtmocks "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1/mocks"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "gitee.com/geekbang/basic-go/webook/reward/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/mock/gomock"
+ "gorm.io/gorm"
+ "testing"
+ "time"
+)
+
+type WechatNativeRewardServiceTestSuite struct {
+ suite.Suite
+ rdb redis.Cmdable
+ db *gorm.DB
+}
+
+func (s *WechatNativeRewardServiceTestSuite) TestPreReward() {
+ t := s.T()
+ testCases := []struct {
+ name string
+ // 实在不想真的跟微信打交道,先保证自己这边没问题
+ mock func(ctrl *gomock.Controller) pmtv1.WechatPaymentServiceClient
+ before func(t *testing.T)
+ after func(t *testing.T)
+
+ r domain.Reward
+
+ wantData string
+ wantErr error
+ }{
+ {
+ name: "直接创建成功",
+ mock: func(ctrl *gomock.Controller) pmtv1.WechatPaymentServiceClient {
+ client := pmtmocks.NewMockWechatPaymentServiceClient(ctrl)
+ client.EXPECT().NativePrePay(gomock.Any(), gomock.Any()).
+ Return(&pmtv1.NativePrePayResponse{
+ CodeUrl: "test_url",
+ }, nil)
+ return client
+ },
+ before: func(t *testing.T) {
+
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ // 验证数据和缓存
+ var r dao.Reward
+ err := s.db.Where("biz = ? AND biz_id = ?", "test", 1).First(&r).Error
+ assert.NoError(t, err)
+ assert.True(t, r.Id > 0)
+ r.Id = 0
+ assert.True(t, r.Ctime > 0)
+ r.Ctime = 0
+ assert.True(t, r.Utime > 0)
+ r.Utime = 0
+ assert.Equal(t, dao.Reward{
+ Biz: "test",
+ BizId: 1,
+ BizName: "测试项目",
+ TargetUid: 1234,
+ Uid: 123,
+ Amount: 1,
+ }, r)
+
+ codeURL, err := s.rdb.GetDel(ctx, s.codeURLKey("test", 1, 123)).Result()
+ require.NoError(t, err)
+ assert.Equal(t, "test_url", codeURL)
+ },
+ r: domain.Reward{
+ Uid: 123,
+ Target: domain.Target{
+ Biz: "test",
+ BizId: 1,
+ BizName: "测试项目",
+ Uid: 1234,
+ },
+ Amt: 1,
+ },
+ wantData: "test_url",
+ },
+ {
+ name: "拿到缓存",
+ mock: func(ctrl *gomock.Controller) pmtv1.WechatPaymentServiceClient {
+ client := pmtmocks.NewMockWechatPaymentServiceClient(ctrl)
+ return client
+ },
+ before: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ err := s.rdb.Set(ctx, s.codeURLKey("test", 2, 123), "test_url_1", time.Minute).Err()
+ require.NoError(t, err)
+ },
+ after: func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ codeURL, err := s.rdb.GetDel(ctx, s.codeURLKey("test", 2, 123)).Result()
+ require.NoError(t, err)
+ assert.Equal(t, "test_url_1", codeURL)
+ },
+ r: domain.Reward{
+ Uid: 123,
+ Target: domain.Target{
+ Biz: "test",
+ BizId: 2,
+ BizName: "测试项目",
+ Uid: 1234,
+ },
+ Amt: 1,
+ },
+ wantData: "test_url_1",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.before(t)
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ svc := startup.InitWechatNativeSvc(tc.mock(ctrl))
+ codeURL, err := svc.PreReward(context.Background(), tc.r)
+ assert.Equal(t, tc.wantErr, err)
+ assert.Equal(t, tc.wantData, codeURL)
+ tc.after(t)
+ })
+ }
+}
+
+func (s *WechatNativeRewardServiceTestSuite) SetupSuite() {
+ s.rdb = startup.InitRedis()
+ s.db = startup.InitTestDB()
+}
+
+func (s *WechatNativeRewardServiceTestSuite) TearDownTest() {
+ s.db.Exec("TRUNCATE TABLE rewards")
+}
+
+func (s *WechatNativeRewardServiceTestSuite) codeURLKey(biz string, bizId, uid int64) string {
+ return fmt.Sprintf("reward:code_url:%s:%d:%d",
+ biz, bizId, uid)
+}
+
+func TestWechatNativeRewardService(t *testing.T) {
+ suite.Run(t, new(WechatNativeRewardServiceTestSuite))
+}
diff --git a/webook/reward/ioc/account.go b/webook/reward/ioc/account.go
new file mode 100644
index 0000000000000000000000000000000000000000..cfe92456b0c607ae18e99930dcb7a9128ab2f073
--- /dev/null
+++ b/webook/reward/ioc/account.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ accountv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/account/v1"
+ "github.com/spf13/viper"
+ etcdv3 "go.etcd.io/etcd/client/v3"
+ "go.etcd.io/etcd/client/v3/naming/resolver"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+func InitAccountClient(etcdClient *etcdv3.Client) accountv1.AccountServiceClient {
+ type Config struct {
+ Target string `json:"target"`
+ Secure bool `json:"secure"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.client.account", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ rs, err := resolver.NewBuilder(etcdClient)
+ if err != nil {
+ panic(err)
+ }
+ opts := []grpc.DialOption{grpc.WithResolvers(rs)}
+ if !cfg.Secure {
+ opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ }
+ cc, err := grpc.Dial(cfg.Target, opts...)
+ if err != nil {
+ panic(err)
+ }
+ return accountv1.NewAccountServiceClient(cc)
+}
diff --git a/webook/reward/ioc/db.go b/webook/reward/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b9d5f7082d62ae5b5a49c77fdcd9bbd1bf6233c
--- /dev/null
+++ b/webook/reward/ioc/db.go
@@ -0,0 +1,31 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func InitDB() *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v1, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{})
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
diff --git a/webook/reward/ioc/etcd.go b/webook/reward/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/reward/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/reward/ioc/grpc.go b/webook/reward/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..f7a26b00a97610c5542c6d335476f2e46c7d4484
--- /dev/null
+++ b/webook/reward/ioc/grpc.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ grpc2 "gitee.com/geekbang/basic-go/webook/reward/grpc"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(reward *grpc2.RewardServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ reward.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "reward",
+ L: l,
+ EtcdClient: ecli,
+ EtcdTTL: cfg.EtcdTTL,
+ }
+}
diff --git a/webook/reward/ioc/log.go b/webook/reward/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/reward/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/reward/ioc/payment.go b/webook/reward/ioc/payment.go
new file mode 100644
index 0000000000000000000000000000000000000000..08685dd69428fa6bd8f7485f7223064bb575f3bf
--- /dev/null
+++ b/webook/reward/ioc/payment.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ "github.com/spf13/viper"
+ etcdv3 "go.etcd.io/etcd/client/v3"
+ "go.etcd.io/etcd/client/v3/naming/resolver"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+func InitPaymentClient(etcdClient *etcdv3.Client) pmtv1.WechatPaymentServiceClient {
+ type Config struct {
+ Target string `json:"target"`
+ Secure bool `json:"secure"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.client.payment", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ rs, err := resolver.NewBuilder(etcdClient)
+ if err != nil {
+ panic(err)
+ }
+ opts := []grpc.DialOption{grpc.WithResolvers(rs)}
+ if !cfg.Secure {
+ opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ }
+ cc, err := grpc.Dial(cfg.Target, opts...)
+ if err != nil {
+ panic(err)
+ }
+ return pmtv1.NewWechatPaymentServiceClient(cc)
+}
diff --git a/webook/reward/ioc/redis.go b/webook/reward/ioc/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..71ea48fc24dd17ebcad0d89dd2c8bd919aa445e3
--- /dev/null
+++ b/webook/reward/ioc/redis.go
@@ -0,0 +1,13 @@
+package ioc
+
+import (
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+)
+
+func InitRedis() redis.Cmdable {
+ cmd := redis.NewClient(&redis.Options{
+ Addr: viper.GetString("redis.addr"),
+ })
+ return cmd
+}
diff --git a/webook/reward/main.go b/webook/reward/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..f41594ed4c86d2886b27d0a8e469a29537c032f6
--- /dev/null
+++ b/webook/reward/main.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ err := app.GRPCServer.Serve()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/webook/reward/repository/cache/redis.go b/webook/reward/repository/cache/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..95e81f6050e4629107ece62cd6a3163ec94442b2
--- /dev/null
+++ b/webook/reward/repository/cache/redis.go
@@ -0,0 +1,44 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+type RewardRedisCache struct {
+ client redis.Cmdable
+}
+
+func NewRewardRedisCache(client redis.Cmdable) RewardCache {
+ return &RewardRedisCache{client: client}
+}
+
+func (c *RewardRedisCache) GetCachedCodeURL(ctx context.Context, r domain.Reward) (domain.CodeURL, error) {
+ key := c.codeURLKey(r)
+ data, err := c.client.Get(ctx, key).Bytes()
+ if err != nil {
+ return domain.CodeURL{}, err
+ }
+ var res domain.CodeURL
+ err = json.Unmarshal(data, &res)
+ return res, err
+}
+
+func (c *RewardRedisCache) CachedCodeURL(ctx context.Context, cu domain.CodeURL, r domain.Reward) error {
+ key := c.codeURLKey(r)
+ data, err := json.Marshal(cu)
+ if err != nil {
+ return err
+ }
+ // 如果你担心 30 分钟刚好是微信订单过期的问题,那么你可以设置成 29 分钟
+ return c.client.Set(ctx, key, data, time.Minute*29).Err()
+}
+
+func (c *RewardRedisCache) codeURLKey(r domain.Reward) string {
+ return fmt.Sprintf("reward:code_url:%s:%d:%d",
+ r.Target.Biz, r.Target.BizId, r.Uid)
+}
diff --git a/webook/reward/repository/cache/types.go b/webook/reward/repository/cache/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..1be79cca0e5b6bb615ecddb2d85cbb214493d1a3
--- /dev/null
+++ b/webook/reward/repository/cache/types.go
@@ -0,0 +1,11 @@
+package cache
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+)
+
+type RewardCache interface {
+ GetCachedCodeURL(ctx context.Context, r domain.Reward) (domain.CodeURL, error)
+ CachedCodeURL(ctx context.Context, cu domain.CodeURL, r domain.Reward) error
+}
diff --git a/webook/reward/repository/dao/gorm.go b/webook/reward/repository/dao/gorm.go
new file mode 100644
index 0000000000000000000000000000000000000000..f8d5e19ab40081e6c522e5eff881df8babc14118
--- /dev/null
+++ b/webook/reward/repository/dao/gorm.go
@@ -0,0 +1,41 @@
+package dao
+
+import (
+ "context"
+ "gorm.io/gorm"
+ "time"
+)
+
+type RewardGORMDAO struct {
+ db *gorm.DB
+}
+
+func (dao *RewardGORMDAO) UpdateStatus(ctx context.Context, rid int64, status uint8) error {
+ return dao.db.WithContext(ctx).
+ Where("id = ?", rid).
+ Updates(map[string]any{
+ "status": status,
+ "utime": time.Now().UnixMilli(),
+ }).Error
+}
+
+func (dao *RewardGORMDAO) GetReward(ctx context.Context, rid int64) (Reward, error) {
+ // 通过 uid 来判定是自己的打赏,防止黑客捞数据
+ var r Reward
+ err := dao.db.WithContext(ctx).
+ Where("id = ? ", rid).
+ First(&r).Error
+ return r, err
+}
+
+func (dao *RewardGORMDAO) Insert(ctx context.Context, r Reward) (int64, error) {
+ now := time.Now().UnixMilli()
+ r.Ctime = now
+ r.Utime = now
+ err := dao.db.WithContext(ctx).Create(&r).Error
+ return r.Id, err
+}
+
+func NewRewardGORMDAO(db *gorm.DB) RewardDAO {
+ return &RewardGORMDAO{db: db}
+}
diff --git a/webook/reward/repository/dao/init.go b/webook/reward/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..b8533f136ed704ec6a29319e1129975a5a2a94c9
--- /dev/null
+++ b/webook/reward/repository/dao/init.go
@@ -0,0 +1,7 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(&Reward{})
+}
diff --git a/webook/reward/repository/dao/types.go b/webook/reward/repository/dao/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..89ed51469cf14971cef8f8652194c1b5f6723187
--- /dev/null
+++ b/webook/reward/repository/dao/types.go
@@ -0,0 +1,28 @@
+package dao
+
+import (
+ "context"
+)
+
+type RewardDAO interface {
+ Insert(ctx context.Context, r Reward) (int64, error)
+ GetReward(ctx context.Context, rid int64) (Reward, error)
+ UpdateStatus(ctx context.Context, rid int64, status uint8) error
+}
+
+type Reward struct {
+ Id int64 `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
+ Biz string `gorm:"index:biz_biz_id"`
+ BizId int64 `gorm:"index:biz_biz_id"`
+ BizName string
+ // 被打赏的人
+ TargetUid int64 `gorm:"index"`
+
+ // 直接采用 RewardStatus 的取值
+ Status uint8
+ // 打赏的人
+ Uid int64
+ Amount int64
+ Ctime int64
+ Utime int64
+}
diff --git a/webook/reward/repository/reward.go b/webook/reward/repository/reward.go
new file mode 100644
index 0000000000000000000000000000000000000000..740639160995e3c61f45937adadde31be077506d
--- /dev/null
+++ b/webook/reward/repository/reward.go
@@ -0,0 +1,70 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+)
+
+type rewardRepository struct {
+ dao dao.RewardDAO
+ cache cache.RewardCache
+}
+
+func (repo *rewardRepository) GetReward(ctx context.Context, rid int64) (domain.Reward, error) {
+ r, err := repo.dao.GetReward(ctx, rid)
+ if err != nil {
+ return domain.Reward{}, err
+ }
+ return repo.toDomain(r), nil
+}
+
+func (repo *rewardRepository) UpdateStatus(ctx context.Context, rid int64, status domain.RewardStatus) error {
+ return repo.dao.UpdateStatus(ctx, rid, status.AsUint8())
+}
+
+func (repo *rewardRepository) GetCachedCodeURL(ctx context.Context, r domain.Reward) (domain.CodeURL, error) {
+ return repo.cache.GetCachedCodeURL(ctx, r)
+}
+
+func (repo *rewardRepository) CachedCodeURL(ctx context.Context, cu domain.CodeURL, r domain.Reward) error {
+ return repo.cache.CachedCodeURL(ctx, cu, r)
+}
+
+func (repo *rewardRepository) CreateReward(
+ ctx context.Context,
+ reward domain.Reward) (int64, error) {
+ return repo.dao.Insert(ctx, repo.toEntity(reward))
+}
+
+func (repo *rewardRepository) toEntity(r domain.Reward) dao.Reward {
+ return dao.Reward{
+ Status: r.Status.AsUint8(),
+ Biz: r.Target.Biz,
+ BizName: r.Target.BizName,
+ BizId: r.Target.BizId,
+ TargetUid: r.Target.Uid,
+ Uid: r.Uid,
+ Amount: r.Amt,
+ }
+}
+
+func (repo *rewardRepository) toDomain(r dao.Reward) domain.Reward {
+ return domain.Reward{
+ Id: r.Id,
+ Uid: r.Uid,
+ Target: domain.Target{
+ Biz: r.Biz,
+ BizId: r.BizId,
+ BizName: r.BizName,
+ Uid: r.Uid,
+ },
+ Amt: r.Amount,
+ Status: domain.RewardStatus(r.Status),
+ }
+}
+
+func NewRewardRepository(dao dao.RewardDAO, c cache.RewardCache) RewardRepository {
+ return &rewardRepository{dao: dao, cache: c}
+}
diff --git a/webook/reward/repository/types.go b/webook/reward/repository/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..82dc8533114739ace3831d783a22d682e579521b
--- /dev/null
+++ b/webook/reward/repository/types.go
@@ -0,0 +1,17 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+)
+
+type RewardRepository interface {
+ CreateReward(ctx context.Context, reward domain.Reward) (int64, error)
+ GetReward(ctx context.Context, rid int64) (domain.Reward, error)
+
+ // GetCachedCodeURL 这两个方法的名字我们明确带上了缓存的字眼
+ // 是希望调用者明白这个是我们缓存下来的,属于业务逻辑的一部分
+ GetCachedCodeURL(ctx context.Context, r domain.Reward) (domain.CodeURL, error)
+ CachedCodeURL(ctx context.Context, cu domain.CodeURL, r domain.Reward) error
+ UpdateStatus(ctx context.Context, rid int64, status domain.RewardStatus) error
+}
diff --git a/webook/reward/service/mocks/reward.mock.go b/webook/reward/service/mocks/reward.mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1aa02b4334811883f27849019f0ccfe47a9d25e
--- /dev/null
+++ b/webook/reward/service/mocks/reward.mock.go
@@ -0,0 +1,56 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./types.go
+//
+// Generated by this command:
+//
+// mockgen -source=./types.go -destination=mocks/reward.mock.go -package=svcmocks RewardService
+//
+// Package svcmocks is a generated GoMock package.
+package svcmocks
+
+import (
+ context "context"
+ reflect "reflect"
+
+ domain "gitee.com/geekbang/basic-go/webook/reward/domain"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockRewardService is a mock of RewardService interface.
+type MockRewardService struct {
+ ctrl *gomock.Controller
+ recorder *MockRewardServiceMockRecorder
+}
+
+// MockRewardServiceMockRecorder is the mock recorder for MockRewardService.
+type MockRewardServiceMockRecorder struct {
+ mock *MockRewardService
+}
+
+// NewMockRewardService creates a new mock instance.
+func NewMockRewardService(ctrl *gomock.Controller) *MockRewardService {
+ mock := &MockRewardService{ctrl: ctrl}
+ mock.recorder = &MockRewardServiceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRewardService) EXPECT() *MockRewardServiceMockRecorder {
+ return m.recorder
+}
+
+// PreReward mocks base method.
+func (m *MockRewardService) PreReward(ctx context.Context, r domain.Reward) (string, int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PreReward", ctx, r)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(int64)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// PreReward indicates an expected call of PreReward.
+func (mr *MockRewardServiceMockRecorder) PreReward(ctx, r any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreReward", reflect.TypeOf((*MockRewardService)(nil).PreReward), ctx, r)
+}
diff --git a/webook/reward/service/types.go b/webook/reward/service/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..1cf915a3a2f8c2a6e728b412efb2d338cd47d67c
--- /dev/null
+++ b/webook/reward/service/types.go
@@ -0,0 +1,17 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+)
+
+//go:generate mockgen -source=./types.go -destination=mocks/reward.mock.go -package=svcmocks RewardService
+type RewardService interface {
+ // PreReward 准备打赏,
+ // 你也可以直接理解为对标到创建一个打赏的订单
+ // 因为目前我们只支持微信扫码支付,所以实际上直接把接口定义成这个样子就可以了
+ PreReward(ctx context.Context,
+ r domain.Reward) (domain.CodeURL, error)
+ GetReward(ctx context.Context, rid, uid int64) (domain.Reward, error)
+ UpdateReward(ctx context.Context, bizTradeNO string, status domain.RewardStatus) error
+}
diff --git a/webook/reward/service/wechat_native.go b/webook/reward/service/wechat_native.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd828031c243c759d526c74110c2e12e727a2925
--- /dev/null
+++ b/webook/reward/service/wechat_native.go
@@ -0,0 +1,184 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ accountv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/account/v1"
+ pmtv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/payment/v1"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/reward/domain"
+ "gitee.com/geekbang/basic-go/webook/reward/repository"
+ "strconv"
+ "strings"
+)
+
+type WechatNativeRewardService struct {
+ client pmtv1.WechatPaymentServiceClient
+ repo repository.RewardRepository
+ l logger.LoggerV1
+ acli accountv1.AccountServiceClient
+}
+
+func (s *WechatNativeRewardService) UpdateReward(ctx context.Context,
+ bizTradeNO string, status domain.RewardStatus) error {
+ rid := s.toRid(bizTradeNO)
+ err := s.repo.UpdateStatus(ctx, rid, status)
+ if err != nil {
+ return err
+ }
+ // 完成了支付,准备入账
+ // 你已经支付成功了
+ if status == domain.RewardStatusPayed {
+ r, err := s.repo.GetReward(ctx, rid)
+ if err != nil {
+ return err
+ }
+ // webook 抽成
+ // 0.1 可以写到数据库里面
+ // 订单计算总价 + 分账
+ // 分账要小心精度问题
+ weAmt := int64(float64(r.Amt) * 0.1)
+ _, err = s.acli.Credit(ctx, &accountv1.CreditRequest{
+ Biz: "reward",
+ BizId: rid,
+ Items: []*accountv1.CreditItem{
+ {
+ AccountType: accountv1.AccountType_AccountTypeReward,
+ // 虽然可能为 0,但是也要记录出来
+ Amt: weAmt,
+ Currency: "CNY",
+ },
+ {
+ Account: r.Uid,
+ Uid: r.Uid,
+ AccountType: accountv1.AccountType_AccountTypeReward,
+ Amt: r.Amt - weAmt,
+ Currency: "CNY",
+ },
+ },
+ })
+ if err != nil {
+ s.l.Error("入账失败了,快来修数据啊!!!",
+ logger.String("biz_trade_no", bizTradeNO),
+ logger.Error(err))
+ // 做好监控和告警,这里
+ // 引入自动修复功能
+ // 如果没有 24小时值班 + 自动修复 + 异地容灾备份(随机演练)
+ // 然后面试官又吹牛逼说自己的可用性有 9999,你就可以认为,他在扯淡。
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *WechatNativeRewardService) GetReward(ctx context.Context, rid, uid int64) (domain.Reward, error) {
+ // 快路径
+ r, err := s.repo.GetReward(ctx, rid)
+ if err != nil {
+ return domain.Reward{}, err
+ }
+ if r.Uid != uid {
+ // 说明是非法查询
+ return domain.Reward{}, errors.New("查询的打赏记录和打赏人对不上")
+ }
+
+ // 有可能,我的打赏记录,还是 Init 状态
+ // 已经是完结状态
+ if r.Completed() || ctx.Value("limited") == "true" {
+ // 我已经知道你的支付结果了
+ return r, nil
+ }
+
+ // 这个时候,考虑到支付到查询结果,我们搞一个慢路径
+ // 你有可能支付了,但是我 reward 本身没有收到通知
+ // 我直接查询 payment,
+ // 只能解决,支付收到了,但是 reward 没收到
+ // 降级状态,限流状态,熔断状态,不要走慢路径
+ resp, err := s.client.GetPayment(ctx, &pmtv1.GetPaymentRequest{
+ BizTradeNo: s.bizTradeNO(r.Id),
+ })
+ if err != nil {
+ // 这边我们直接返回从数据库查询的数据
+ s.l.Error("慢路径查询支付结果失败",
+ logger.Int64("rid", r.Id), logger.Error(err))
+ return r, nil
+ }
+ // 更新状态
+ switch resp.Status {
+ case pmtv1.PaymentStatus_PaymentStatusFailed:
+ r.Status = domain.RewardStatusFailed
+ case pmtv1.PaymentStatus_PaymentStatusInit:
+ r.Status = domain.RewardStatusInit
+ case pmtv1.PaymentStatus_PaymentStatusSuccess:
+ r.Status = domain.RewardStatusPayed
+ case pmtv1.PaymentStatus_PaymentStatusRefund:
+ // 理论上来说不可能出现这个,直接设置为失败
+ r.Status = domain.RewardStatusFailed
+ }
+ err = s.repo.UpdateStatus(ctx, rid, r.Status)
+ if err != nil {
+ s.l.Error("更新本地打赏状态失败",
+ logger.Int64("rid", r.Id), logger.Error(err))
+ return r, nil
+ }
+ return r, nil
+}
+
+func (s *WechatNativeRewardService) PreReward(ctx context.Context, r domain.Reward) (domain.CodeURL, error) {
+ // 可以考虑缓存我的二维码,一旦我发现支付成功了,我就清除我的二维码
+ cu, err := s.repo.GetCachedCodeURL(ctx, r)
+ if err == nil {
+ return cu, nil
+ }
+ r.Status = domain.RewardStatusInit
+ rid, err := s.repo.CreateReward(ctx, r)
+ if err != nil {
+ return domain.CodeURL{}, err
+ }
+ // 我在这里记录分账信息
+ resp, err := s.client.NativePrePay(ctx, &pmtv1.PrePayRequest{
+ Amt: &pmtv1.Amount{
+ Total: r.Amt,
+ Currency: "CNY",
+ },
+ //PayDetail: [
+ //{"account": "platform", amt: 100,}
+ //{"account": uid-123,, amt: 900}
+ //],
+ BizTradeNo: fmt.Sprintf("reward-%d", rid),
+ Description: fmt.Sprintf("打赏-%s", r.Target.BizName),
+ })
+ if err != nil {
+ return domain.CodeURL{}, err
+ }
+ cu = domain.CodeURL{
+ Rid: rid,
+ URL: resp.CodeUrl,
+ }
+ // 当然可以异步了
+ err1 := s.repo.CachedCodeURL(ctx, cu, r)
+ if err1 != nil {
+ // 记录日志
+ }
+ return cu, nil
+}
+
+func (s *WechatNativeRewardService) bizTradeNO(rid int64) string {
+ return fmt.Sprintf("reward-%d", rid)
+}
+
+func (s *WechatNativeRewardService) toRid(tradeNO string) int64 {
+ ridStr := strings.Split(tradeNO, "-")
+ val, _ := strconv.ParseInt(ridStr[1], 10, 64)
+ return val
+}
+
+func NewWechatNativeRewardService(
+ client pmtv1.WechatPaymentServiceClient,
+ repo repository.RewardRepository,
+ l logger.LoggerV1,
+ acli accountv1.AccountServiceClient,
+) RewardService {
+ return &WechatNativeRewardService{client: client, repo: repo, l: l, acli: acli}
+}
diff --git a/webook/reward/wire.go b/webook/reward/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..d3b282ac296d27169a2fd5d0dfb8deb2b19afe8c
--- /dev/null
+++ b/webook/reward/wire.go
@@ -0,0 +1,35 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+ "gitee.com/geekbang/basic-go/webook/reward/grpc"
+ "gitee.com/geekbang/basic-go/webook/reward/ioc"
+ "gitee.com/geekbang/basic-go/webook/reward/repository"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "github.com/google/wire"
+)
+
+var thirdPartySet = wire.NewSet(
+ ioc.InitDB,
+ ioc.InitLogger,
+ ioc.InitEtcdClient,
+ ioc.InitRedis)
+
+func Init() *wego.App {
+ wire.Build(thirdPartySet,
+ service.NewWechatNativeRewardService,
+ ioc.InitAccountClient,
+ ioc.InitGRPCxServer,
+ ioc.InitPaymentClient,
+ repository.NewRewardRepository,
+ cache.NewRewardRedisCache,
+ dao.NewRewardGORMDAO,
+ grpc.NewRewardServiceServer,
+ wire.Struct(new(wego.App), "GRPCServer"),
+ )
+ return new(wego.App)
+}
diff --git a/webook/reward/wire_gen.go b/webook/reward/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..978b4e22048440bb2fe384a47ea9a9c8b64d6485
--- /dev/null
+++ b/webook/reward/wire_gen.go
@@ -0,0 +1,43 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+ "gitee.com/geekbang/basic-go/webook/reward/grpc"
+ "gitee.com/geekbang/basic-go/webook/reward/ioc"
+ "gitee.com/geekbang/basic-go/webook/reward/repository"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/reward/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/reward/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func Init() *wego.App {
+ client := ioc.InitEtcdClient()
+ wechatPaymentServiceClient := ioc.InitPaymentClient(client)
+ db := ioc.InitDB()
+ rewardDAO := dao.NewRewardGORMDAO(db)
+ cmdable := ioc.InitRedis()
+ rewardCache := cache.NewRewardRedisCache(cmdable)
+ rewardRepository := repository.NewRewardRepository(rewardDAO, rewardCache)
+ loggerV1 := ioc.InitLogger()
+ accountServiceClient := ioc.InitAccountClient(client)
+ rewardService := service.NewWechatNativeRewardService(wechatPaymentServiceClient, rewardRepository, loggerV1, accountServiceClient)
+ rewardServiceServer := grpc.NewRewardServiceServer(rewardService)
+ server := ioc.InitGRPCxServer(rewardServiceServer, client, loggerV1)
+ app := &wego.App{
+ GRPCServer: server,
+ }
+ return app
+}
+
+// wire.go:
+
+var thirdPartySet = wire.NewSet(ioc.InitDB, ioc.InitLogger, ioc.InitEtcdClient, ioc.InitRedis)
diff --git a/webook/script/canal/canal.properties b/webook/script/canal/canal.properties
new file mode 100644
index 0000000000000000000000000000000000000000..b042c7cc90b2ed6eb395bc13ad8fb231dc15d093
--- /dev/null
+++ b/webook/script/canal/canal.properties
@@ -0,0 +1,188 @@
+#################################################
+######### common argument #############
+#################################################
+# tcp bind ip
+canal.ip =
+# register ip to zookeeper
+canal.register.ip =
+canal.port = 11111
+canal.metrics.pull.port = 11112
+# canal instance user/passwd
+# canal.user = canal
+# canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458
+
+# canal admin config
+#canal.admin.manager = 127.0.0.1:8089
+canal.admin.port = 11110
+canal.admin.user = admin
+canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
+# admin auto register
+#canal.admin.register.auto = true
+#canal.admin.register.cluster =
+#canal.admin.register.name =
+
+canal.zkServers =
+# flush data to zk
+canal.zookeeper.flush.period = 1000
+canal.withoutNetty = false
+# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
+canal.serverMode = kafka
+# flush meta cursor/parse position to file
+canal.file.data.dir = ${canal.conf.dir}
+canal.file.flush.period = 1000
+## memory store RingBuffer size, should be Math.pow(2,n)
+canal.instance.memory.buffer.size = 16384
+## memory store RingBuffer used memory unit size , default 1kb
+canal.instance.memory.buffer.memunit = 1024
+## meory store gets mode used MEMSIZE or ITEMSIZE
+canal.instance.memory.batch.mode = MEMSIZE
+canal.instance.memory.rawEntry = true
+
+## detecing config
+canal.instance.detecting.enable = false
+#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
+canal.instance.detecting.sql = select 1
+canal.instance.detecting.interval.time = 3
+canal.instance.detecting.retry.threshold = 3
+canal.instance.detecting.heartbeatHaEnable = false
+
+# support maximum transaction size, more than the size of the transaction will be cut into multiple transations delivery
+canal.instance.transaction.size = 1024
+# mysql fallback connected to new master should fallback times
+canal.instance.fallbackIntervalInSeconds = 60
+
+# network config
+canal.instance.network.receiveBufferSize = 16384
+canal.instance.network.sendBufferSize = 16384
+canal.instance.network.soTimeout = 30
+
+# binlog filter config
+canal.instance.filter.druid.ddl = false
+canal.instance.filter.query.dcl = false
+canal.instance.filter.query.dml = false
+canal.instance.filter.query.ddl = false
+canal.instance.filter.table.error = false
+canal.instance.filter.rows = false
+canal.instance.filter.transaction.entry = false
+canal.instance.filter.dml.insert = false
+canal.instance.filter.dml.update = false
+canal.instance.filter.dml.delete = false
+
+# binlog format/image check
+canal.instance.binlog.format = ROW,STATEMENT,MIXED
+canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
+
+# binlog ddl isolation
+canal.instance.get.ddl.isolation = false
+
+# parallel parser config
+canal.instance.parser.parallel = true
+## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
+#canal.instance.parser.parallelThreadSize = 16
+## disruptor ringbuffer size, must be power of 2
+canal.instance.parser.parallelBufferSize = 256
+
+# table meta tsdb info
+# time serial database
+canal.instance.tsdb.enable = true
+canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
+canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
+canal.instance.tsdb.dbUsername = canal
+canal.instance.tsdb.dbPassword = canal
+# dump snapshot interval, default 24 hour
+canal.instance.tsdb.snapshot.interval = 24
+# purge snapshot expire , default 360 hour(15 days)
+canal.instance.tsdb.snapshot.expire = 360
+
+#################################################
+######### destinations #############
+#################################################
+canal.destinations = webook
+# conf root dir
+canal.conf.dir = ../conf
+# auto scan instance dir add/remove and start/stop instance
+canal.auto.scan = true
+canal.auto.scan.interval = 5
+# set this value to 'true' means that when binlog pos not found, skip to latest.
+# WARN: pls keep 'false' in production env, or if you know what you want.
+canal.auto.reset.latest.pos.mode = false
+
+canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
+#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
+
+canal.instance.global.mode = spring
+canal.instance.global.lazy = false
+canal.instance.global.manager.address = ${canal.admin.manager}
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+
+##################################################
+######### MQ Properties #############
+##################################################
+# aliyun ak/sk , support rds/mq
+#canal.aliyun.accessKey =
+#canal.aliyun.secretKey =
+#canal.aliyun.uid=
+
+canal.mq.flatMessage = true
+canal.mq.canalBatchSize = 50
+canal.mq.canalGetTimeout = 100
+# Set this value to "cloud", if you want open message trace feature in aliyun.
+canal.mq.accessChannel = local
+
+canal.mq.database.hash = true
+canal.mq.send.thread.size = 30
+canal.mq.build.thread.size = 8
+
+##################################################
+######### Kafka #############
+##################################################
+kafka.bootstrap.servers = kafka:9092
+kafka.acks = all
+kafka.compression.type = none
+kafka.batch.size = 16384
+kafka.linger.ms = 1
+kafka.max.request.size = 1048576
+kafka.buffer.memory = 33554432
+kafka.max.in.flight.requests.per.connection = 1
+kafka.retries = 0
+
+#kafka.kerberos.enable = false
+#kafka.kerberos.krb5.file = ../conf/kerberos/krb5.conf
+#kafka.kerberos.jaas.file = ../conf/kerberos/jaas.conf
+
+# sasl demo
+# kafka.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required \\n username=\"alice\" \\npassword="alice-secret\";
+# kafka.sasl.mechanism = SCRAM-SHA-512
+# kafka.security.protocol = SASL_PLAINTEXT
+
+##################################################
+######### RocketMQ #############
+##################################################
+#rocketmq.producer.group = test
+#rocketmq.enable.message.trace = false
+#rocketmq.customized.trace.topic =
+#rocketmq.namespace =
+#rocketmq.namesrv.addr = 127.0.0.1:9876
+#rocketmq.retry.times.when.send.failed = 0
+#rocketmq.vip.channel.enabled = false
+#rocketmq.tag =
+
+##################################################
+######### RabbitMQ #############
+##################################################
+#rabbitmq.host =
+#rabbitmq.virtual.host =
+#rabbitmq.exchange =
+#rabbitmq.username =
+#rabbitmq.password =
+#rabbitmq.deliveryMode =
+
+
+##################################################
+######### Pulsar #############
+##################################################
+#pulsarmq.serverUrl =
+#pulsarmq.roleToken =
+#pulsarmq.topicTenantPrefix =
diff --git a/webook/script/canal/webook/instance.properties b/webook/script/canal/webook/instance.properties
new file mode 100644
index 0000000000000000000000000000000000000000..30c3507b24dca84edb31eb1bf195934db483c5f2
--- /dev/null
+++ b/webook/script/canal/webook/instance.properties
@@ -0,0 +1,64 @@
+#################################################
+## mysql serverId , v1.0.26+ will autoGen
+canal.instance.mysql.slaveId=1234
+
+# enable gtid use true/false
+canal.instance.gtidon=false
+
+# position info
+canal.instance.master.address=mysql8:3306
+canal.instance.master.journal.name=
+canal.instance.master.position=
+canal.instance.master.timestamp=
+canal.instance.master.gtid=
+
+# rds oss binlog
+#canal.instance.rds.accesskey=
+#canal.instance.rds.secretkey=
+#canal.instance.rds.instanceId=
+
+# table meta tsdb info
+#canal.instance.tsdb.enable=true
+#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
+#canal.instance.tsdb.dbUsername=canal
+#canal.instance.tsdb.dbPassword=canal
+
+#canal.instance.standby.address =
+#canal.instance.standby.journal.name =
+#canal.instance.standby.position =
+#canal.instance.standby.timestamp =
+#canal.instance.standby.gtid=
+
+# username/password
+canal.instance.dbUsername=canal
+canal.instance.dbPassword=canal
+canal.instance.connectionCharset = UTF-8
+# enable druid Decrypt database password
+canal.instance.enableDruid=false
+#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==
+
+# table regex
+canal.instance.filter.regex=.*\\..*
+# table black regex
+canal.instance.filter.black.regex=mysql\\.slave_.*
+# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
+#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
+# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
+#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch
+
+
+# mq config
+canal.mq.topic=webook_binlog
+# dynamic topic route by schema or table regex
+#canal.mq.dynamicTopic=*\\..*
+#canal.mq.partition=0
+# hash partition config
+#canal.mq.enableDynamicQueuePartition=false
+canal.mq.partitionsNum=3
+#canal.mq.dynamicTopicPartitionNum=test.*:4,mycanal:6
+# ?? id ???
+canal.mq.partitionHash=.*\\..*:id
+#
+# multi stream for polardbx
+canal.instance.multi.stream.on=false
+#################################################
diff --git a/webook/script/mysql/init.sql b/webook/script/mysql/init.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e4f5e114ed992234a87f34ed0fbdefc57b6289d1
--- /dev/null
+++ b/webook/script/mysql/init.sql
@@ -0,0 +1,13 @@
+create database webook;
+create database webook_intr;
+create database webook_article;
+create database webook_user;
+create database webook_payment;
+create database webook_account;
+create database webook_reward;
+create database webook_comment;
+create database webook_tag;
+
+# 准备 canal 用户
+CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
+GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' WITH GRANT OPTION;
\ No newline at end of file
diff --git a/webook/search/config/dev.yaml b/webook/search/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..076ce2a8d6f25831419279c9529dafaa5122573f
--- /dev/null
+++ b/webook/search/config/dev.yaml
@@ -0,0 +1,17 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook"
+
+redis:
+ addr: "localhost:6379"
+
+kafka:
+ addrs:
+ - "localhost:9094"
+
+grpc:
+# 启动监听 8090 端口
+ addr: ":8090"
+
+es:
+ urls: "https://localhost:9200"
+ sniff: false
\ No newline at end of file
diff --git a/webook/search/domain/article.go b/webook/search/domain/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..65ca054a2469eb1113de6fdc30f415df6993cd77
--- /dev/null
+++ b/webook/search/domain/article.go
@@ -0,0 +1,9 @@
+package domain
+
+type Article struct {
+ Id int64
+ Title string
+ Status int32
+ Content string
+ Tags []string
+}
diff --git a/webook/search/domain/search_result.go b/webook/search/domain/search_result.go
new file mode 100644
index 0000000000000000000000000000000000000000..feeeb50953d3b6bdda5922599182a371a08d60ca
--- /dev/null
+++ b/webook/search/domain/search_result.go
@@ -0,0 +1,6 @@
+package domain
+
+type SearchResult struct {
+ Users []User
+ Articles []Article
+}
diff --git a/webook/search/domain/user.go b/webook/search/domain/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..0082145ad96e635d73380fe7dc843a96674dad51
--- /dev/null
+++ b/webook/search/domain/user.go
@@ -0,0 +1,8 @@
+package domain
+
+type User struct {
+ Id int64
+ Email string
+ Nickname string
+ Phone string
+}
diff --git a/webook/search/events/article.go b/webook/search/events/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8ebc33571c34b910c6a23004de032ffbdee52b5
--- /dev/null
+++ b/webook/search/events/article.go
@@ -0,0 +1,69 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+const topicSyncArticle = "sync_article_event"
+
+type ArticleConsumer struct {
+ syncSvc service.SyncService
+ client sarama.Client
+ l logger.LoggerV1
+}
+
+func NewArticleConsumer(client sarama.Client,
+ l logger.LoggerV1,
+ svc service.SyncService) *ArticleConsumer {
+ return &ArticleConsumer{
+ syncSvc: svc,
+ client: client,
+ l: l,
+ }
+}
+
+type ArticleEvent struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Status int32 `json:"status"`
+ Content string `json:"content"`
+}
+
+func (a *ArticleConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("sync_article",
+ a.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{topicSyncArticle},
+ saramax.NewHandler[ArticleEvent](a.l, a.Consume))
+ if err != nil {
+ a.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (a *ArticleConsumer) Consume(sg *sarama.ConsumerMessage,
+ evt ArticleEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return a.syncSvc.InputArticle(ctx, a.toDomain(evt))
+}
+
+func (a *ArticleConsumer) toDomain(article ArticleEvent) domain.Article {
+ return domain.Article{
+ Id: article.Id,
+ Title: article.Title,
+ Status: article.Status,
+ Content: article.Content,
+ }
+}
diff --git a/webook/search/events/search.go b/webook/search/events/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..fefd8fcb10ccb9c31cead98c5760f5df618301f4
--- /dev/null
+++ b/webook/search/events/search.go
@@ -0,0 +1,51 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+type SyncDataEvent struct {
+ IndexName string
+ DocID string
+ Data string
+ // 假如说用于同步 user
+ // IndexName = user_index
+ // DocID = "123"
+ // Data = {"id": 123, "email":xx, nickname: ""}
+}
+
+type SyncDataEventConsumer struct {
+ svc service.SyncService
+ client sarama.Client
+ l logger.LoggerV1
+}
+
+func (a *SyncDataEventConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("search_sync_data",
+ a.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{topicSyncArticle},
+ saramax.NewHandler[SyncDataEvent](a.l, a.Consume))
+ if err != nil {
+ a.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (a *SyncDataEventConsumer) Consume(sg *sarama.ConsumerMessage,
+ evt SyncDataEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ // 在这里执行转化
+ return a.svc.InputAny(ctx, evt.IndexName, evt.DocID, evt.Data)
+}
diff --git a/webook/search/events/types.go b/webook/search/events/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec4622544c2dc70b04f57c12375836b106c41d88
--- /dev/null
+++ b/webook/search/events/types.go
@@ -0,0 +1,5 @@
+package events
+
+type Consumer interface {
+ Start() error
+}
diff --git a/webook/search/events/user.go b/webook/search/events/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..b7bb2d25295a899d23b1da0e09a566e19f954f9c
--- /dev/null
+++ b/webook/search/events/user.go
@@ -0,0 +1,69 @@
+package events
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/IBM/sarama"
+ "time"
+)
+
+const topicSyncUser = "sync_user_event"
+
+type UserConsumer struct {
+ syncSvc service.SyncService
+ client sarama.Client
+ l logger.LoggerV1
+}
+
+type UserEvent struct {
+ Id int64 `json:"id"`
+ Email string `json:"email"`
+ Phone string `json:"phone"`
+ Nickname string `json:"nickname"`
+}
+
+func NewUserConsumer(client sarama.Client,
+ l logger.LoggerV1,
+ svc service.SyncService) *UserConsumer {
+ return &UserConsumer{
+ syncSvc: svc,
+ client: client,
+ l: l,
+ }
+}
+
+func (u *UserConsumer) Start() error {
+ cg, err := sarama.NewConsumerGroupFromClient("sync_user",
+ u.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{topicSyncUser},
+ saramax.NewHandler[UserEvent](u.l, u.Consume))
+ if err != nil {
+ u.l.Error("退出了消费循环异常", logger.Error(err))
+ }
+ }()
+ return err
+}
+
+func (u *UserConsumer) Consume(sg *sarama.ConsumerMessage,
+ evt UserEvent) error {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ return u.syncSvc.InputUser(ctx, u.toDomain(evt))
+}
+
+func (u *UserConsumer) toDomain(evt UserEvent) domain.User {
+ return domain.User{
+ Id: evt.Id,
+ Email: evt.Email,
+ Nickname: evt.Nickname,
+ Phone: evt.Phone,
+ }
+}
diff --git a/webook/search/grpc/search.go b/webook/search/grpc/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..137c70103bec368eebe36f517317485c1789b027
--- /dev/null
+++ b/webook/search/grpc/search.go
@@ -0,0 +1,56 @@
+package grpc
+
+import (
+ "context"
+ searchv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/search/v1"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/ecodeclub/ekit/slice"
+ "google.golang.org/grpc"
+)
+
+type SearchServiceServer struct {
+ searchv1.UnimplementedSearchServiceServer
+ svc service.SearchService
+}
+
+func NewSearchService(svc service.SearchService) *SearchServiceServer {
+ return &SearchServiceServer{svc: svc}
+}
+
+func (s *SearchServiceServer) Register(server grpc.ServiceRegistrar) {
+ searchv1.RegisterSearchServiceServer(server, s)
+}
+
+//func (s *SearchServiceServer) SearchUserByAgent(ctx context.Context,) {
+//
+//}
+
+func (s *SearchServiceServer) Search(ctx context.Context, request *searchv1.SearchRequest) (*searchv1.SearchResponse, error) {
+ resp, err := s.svc.Search(ctx, request.Uid, request.Expression)
+ if err != nil {
+ return nil, err
+ }
+ return &searchv1.SearchResponse{
+ User: &searchv1.UserResult{
+ Users: slice.Map(resp.Users, func(idx int, src domain.User) *searchv1.User {
+ return &searchv1.User{
+ Id: src.Id,
+ Nickname: src.Nickname,
+ Email: src.Email,
+ Phone: src.Phone,
+ }
+ }),
+ },
+ Article: &searchv1.ArticleResult{
+ Articles: slice.Map(resp.Articles, func(idx int, src domain.Article) *searchv1.Article {
+ return &searchv1.Article{
+ Id: src.Id,
+ Title: src.Title,
+ Status: src.Status,
+ Content: src.Content,
+ }
+ }),
+ },
+ }, nil
+}
diff --git a/webook/search/grpc/sync.go b/webook/search/grpc/sync.go
new file mode 100644
index 0000000000000000000000000000000000000000..2d831de1fa1fbe7f8075a4370d43bd2d78660919
--- /dev/null
+++ b/webook/search/grpc/sync.go
@@ -0,0 +1,58 @@
+package grpc
+
+import (
+ "context"
+ searchv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/search/v1"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "google.golang.org/grpc"
+)
+
+type SyncServiceServer struct {
+ searchv1.UnimplementedSyncServiceServer
+ syncSvc service.SyncService
+}
+
+func NewSyncServiceServer(syncSvc service.SyncService) *SyncServiceServer {
+ return &SyncServiceServer{
+ syncSvc: syncSvc,
+ }
+}
+
+func (s *SyncServiceServer) Register(server grpc.ServiceRegistrar) {
+ searchv1.RegisterSyncServiceServer(server, s)
+}
+
+// InputUser 业务专属接口,你可以高度定制化
+func (s *SyncServiceServer) InputUser(ctx context.Context, request *searchv1.InputUserRequest) (*searchv1.InputUserResponse, error) {
+ err := s.syncSvc.InputUser(ctx, s.toDomainUser(request.GetUser()))
+ return &searchv1.InputUserResponse{}, err
+}
+
+func (s *SyncServiceServer) InputArticle(ctx context.Context, request *searchv1.InputArticleRequest) (*searchv1.InputArticleResponse, error) {
+ err := s.syncSvc.InputArticle(ctx, s.toDomainArticle(request.GetArticle()))
+ return &searchv1.InputArticleResponse{}, err
+}
+
+func (s *SyncServiceServer) InputAny(ctx context.Context, req *searchv1.InputAnyRequest) (*searchv1.InputAnyResponse, error) {
+ err := s.syncSvc.InputAny(ctx, req.IndexName, req.DocId, req.Data)
+ return &searchv1.InputAnyResponse{}, err
+}
+
+func (s *SyncServiceServer) toDomainUser(vuser *searchv1.User) domain.User {
+ return domain.User{
+ Id: vuser.Id,
+ Email: vuser.Email,
+ Nickname: vuser.Nickname,
+ }
+}
+
+func (s *SyncServiceServer) toDomainArticle(art *searchv1.Article) domain.Article {
+ return domain.Article{
+ Id: art.Id,
+ Title: art.Title,
+ Status: art.Status,
+ Content: art.Content,
+ Tags: art.Tags,
+ }
+}
diff --git a/webook/search/integration/search_test.go b/webook/search/integration/search_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd45a88efae162f56a9ac56197957e7f50b3a922
--- /dev/null
+++ b/webook/search/integration/search_test.go
@@ -0,0 +1,86 @@
+package integration
+
+import (
+ "context"
+ "encoding/json"
+ searchv1 "gitee.com/geekbang/basic-go/webook/api/proto/gen/search/v1"
+ "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "gitee.com/geekbang/basic-go/webook/search/integration/startup"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "testing"
+ "time"
+)
+
+type SearchTestSuite struct {
+ suite.Suite
+ searchSvc *grpc.SearchServiceServer
+ syncSvc *grpc.SyncServiceServer
+}
+
+func (s *SearchTestSuite) SetupSuite() {
+ s.searchSvc = startup.InitSearchServer()
+ s.syncSvc = startup.InitSyncServer()
+}
+
+func (s *SearchTestSuite) TestSearch() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ data, err := json.Marshal(BizTags{
+ Uid: 1001,
+ Biz: "article",
+ BizId: 123,
+ Tags: []string{"Jerry"},
+ })
+ require.NoError(s.T(), err)
+ _, err = s.syncSvc.InputAny(ctx, &searchv1.InputAnyRequest{
+ IndexName: "tags_index",
+ DocId: "abcd",
+ Data: string(data),
+ })
+ require.NoError(s.T(), err)
+
+ _, err = s.syncSvc.InputUser(ctx, &searchv1.InputUserRequest{
+ User: &searchv1.User{
+ Id: 123,
+ Nickname: "Tom White",
+ },
+ })
+ require.NoError(s.T(), err)
+ _, err = s.syncSvc.InputArticle(ctx, &searchv1.InputArticleRequest{
+ Article: &searchv1.Article{
+ Id: 123,
+ Title: "Tom 的小秘密",
+ Status: 2,
+ },
+ })
+ require.NoError(s.T(), err)
+ _, err = s.syncSvc.InputArticle(ctx, &searchv1.InputArticleRequest{
+ Article: &searchv1.Article{
+ Id: 124,
+ Content: "这是内容,Tom 的小秘密",
+ Status: 2,
+ },
+ })
+ require.NoError(s.T(), err)
+ resp, err := s.searchSvc.Search(ctx, &searchv1.SearchRequest{
+ Expression: "Tom 内容 Jerry",
+ Uid: 1001,
+ })
+ require.NoError(s.T(), err)
+ assert.Equal(s.T(), 1, len(resp.User.Users))
+ assert.Equal(s.T(), 2, len(resp.Article.Articles))
+}
+
+type BizTags struct {
+ Uid int64 `json:"uid"`
+ Biz string `json:"biz"`
+ BizId int64 `json:"biz_id"`
+ Tags []string `json:"tags"`
+}
+
+func TestSearchService(t *testing.T) {
+ suite.Run(t, new(SearchTestSuite))
+}
diff --git a/webook/search/integration/startup/es.go b/webook/search/integration/startup/es.go
new file mode 100644
index 0000000000000000000000000000000000000000..210ba58efb387203630f0d8f599aaa2a7d09fa49
--- /dev/null
+++ b/webook/search/integration/startup/es.go
@@ -0,0 +1,27 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "github.com/olivere/elastic/v7"
+ "log"
+ "time"
+)
+
+func InitESClient() *elastic.Client {
+ const timeout = 10 * time.Second
+ opts := []elastic.ClientOptionFunc{
+ elastic.SetURL("http://localhost:9200"),
+ elastic.SetSniff(false),
+ elastic.SetHealthcheckTimeoutStartup(timeout),
+ elastic.SetTraceLog(log.Default()),
+ }
+ client, err := elastic.NewClient(opts...)
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitES(client)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/search/integration/startup/wire.go b/webook/search/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..83faac389f17a50dd0dadb91fe698b2bef91ae92
--- /dev/null
+++ b/webook/search/integration/startup/wire.go
@@ -0,0 +1,46 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "gitee.com/geekbang/basic-go/webook/search/ioc"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewUserElasticDAO,
+ dao.NewArticleElasticDAO,
+ dao.NewTagESDAO,
+ dao.NewAnyESDAO,
+ repository.NewUserRepository,
+ repository.NewAnyRepository,
+ repository.NewArticleRepository,
+ service.NewSyncService,
+ service.NewSearchService,
+)
+
+var thirdProvider = wire.NewSet(
+ InitESClient,
+ ioc.InitLogger)
+
+func InitSearchServer() *grpc.SearchServiceServer {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ grpc.NewSearchService,
+ )
+ return new(grpc.SearchServiceServer)
+}
+
+func InitSyncServer() *grpc.SyncServiceServer {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ grpc.NewSyncServiceServer,
+ )
+ return new(grpc.SyncServiceServer)
+}
diff --git a/webook/search/integration/startup/wire_gen.go b/webook/search/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..ecdcf40cf583b2b9101d38d36719b9a669f32a99
--- /dev/null
+++ b/webook/search/integration/startup/wire_gen.go
@@ -0,0 +1,52 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "gitee.com/geekbang/basic-go/webook/search/ioc"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func InitSearchServer() *grpc.SearchServiceServer {
+ client := InitESClient()
+ userDAO := dao.NewUserElasticDAO(client)
+ userRepository := repository.NewUserRepository(userDAO)
+ articleDAO := dao.NewArticleElasticDAO(client)
+ tagDAO := dao.NewTagESDAO(client)
+ articleRepository := repository.NewArticleRepository(articleDAO, tagDAO)
+ searchService := service.NewSearchService(userRepository, articleRepository)
+ searchServiceServer := grpc.NewSearchService(searchService)
+ return searchServiceServer
+}
+
+func InitSyncServer() *grpc.SyncServiceServer {
+ client := InitESClient()
+ anyDAO := dao.NewAnyESDAO(client)
+ anyRepository := repository.NewAnyRepository(anyDAO)
+ userDAO := dao.NewUserElasticDAO(client)
+ userRepository := repository.NewUserRepository(userDAO)
+ articleDAO := dao.NewArticleElasticDAO(client)
+ tagDAO := dao.NewTagESDAO(client)
+ articleRepository := repository.NewArticleRepository(articleDAO, tagDAO)
+ syncService := service.NewSyncService(anyRepository, userRepository, articleRepository)
+ syncServiceServer := grpc.NewSyncServiceServer(syncService)
+ return syncServiceServer
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewUserElasticDAO, dao.NewArticleElasticDAO, dao.NewTagESDAO, dao.NewAnyESDAO, repository.NewUserRepository, repository.NewAnyRepository, repository.NewArticleRepository, service.NewSyncService, service.NewSearchService)
+
+var thirdProvider = wire.NewSet(
+ InitESClient, ioc.InitLogger,
+)
diff --git a/webook/search/ioc/es.go b/webook/search/ioc/es.go
new file mode 100644
index 0000000000000000000000000000000000000000..6e25ff0e573ecffc3170537f53c8f0f160e0f349
--- /dev/null
+++ b/webook/search/ioc/es.go
@@ -0,0 +1,36 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "github.com/olivere/elastic/v7"
+ "github.com/spf13/viper"
+ "time"
+)
+
+// InitESClient 读取配置文件,进行初始化ES客户端
+func InitESClient() *elastic.Client {
+ type Config struct {
+ Url string `yaml:"url"`
+ Sniff bool `yaml:"sniff"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("es", &cfg)
+ if err != nil {
+ panic(fmt.Errorf("读取 ES 配置失败 %w", err))
+ }
+ const timeout = 100 * time.Second
+ opts := []elastic.ClientOptionFunc{
+ elastic.SetURL(cfg.Url),
+ elastic.SetHealthcheckTimeoutStartup(timeout),
+ }
+ client, err := elastic.NewClient(opts...)
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitES(client)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/search/ioc/etcd.go b/webook/search/ioc/etcd.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cb53d08f84544381f0c13ece4e3aacfcddea649
--- /dev/null
+++ b/webook/search/ioc/etcd.go
@@ -0,0 +1,19 @@
+package ioc
+
+import (
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func InitEtcdClient() *clientv3.Client {
+ var cfg clientv3.Config
+ err := viper.UnmarshalKey("etcd", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := clientv3.New(cfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
diff --git a/webook/search/ioc/grpc.go b/webook/search/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..274cb637afa60d5393188c7d7792cbb9f73dda20
--- /dev/null
+++ b/webook/search/ioc/grpc.go
@@ -0,0 +1,37 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ grpc2 "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(syncRpc *grpc2.SyncServiceServer,
+ searchRpc *grpc2.SearchServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ syncRpc.Register(server)
+ searchRpc.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "user",
+ L: l,
+ EtcdTTL: cfg.EtcdTTL,
+ EtcdClient: ecli,
+ }
+}
diff --git a/webook/search/ioc/kafka.go b/webook/search/ioc/kafka.go
new file mode 100644
index 0000000000000000000000000000000000000000..881d1ebf2d287cb0573bd95f816eab1c6ee2f8a2
--- /dev/null
+++ b/webook/search/ioc/kafka.go
@@ -0,0 +1,33 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/events"
+ "github.com/IBM/sarama"
+ "github.com/spf13/viper"
+)
+
+func InitKafka() sarama.Client {
+ type Config struct {
+ Addrs []string `yaml:"addrs"`
+ }
+ saramaCfg := sarama.NewConfig()
+ saramaCfg.Producer.Return.Successes = true
+ var cfg Config
+ err := viper.UnmarshalKey("kafka", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ client, err := sarama.NewClient(cfg.Addrs, saramaCfg)
+ if err != nil {
+ panic(err)
+ }
+ return client
+}
+
+// NewConsumers 面临的问题依旧是所有的 Consumer 在这里注册一下
+func NewConsumers(articleConsumer *events.ArticleConsumer, userConsumer *events.UserConsumer) []events.Consumer {
+ return []events.Consumer{
+ articleConsumer,
+ userConsumer,
+ }
+}
diff --git a/webook/search/ioc/log.go b/webook/search/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/search/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/search/main.go b/webook/search/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e9b8c45e1d9265b87ccd4fb617b8adc98d5eda3
--- /dev/null
+++ b/webook/search/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/search/events"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ app := Init()
+ for _, c := range app.consumers {
+ err := c.Start()
+ if err != nil {
+ panic(err)
+ }
+ }
+ err := app.server.Serve()
+ panic(err)
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
+
+type App struct {
+ server *grpcx.Server
+ consumers []events.Consumer
+}
diff --git a/webook/search/repository/any.go b/webook/search/repository/any.go
new file mode 100644
index 0000000000000000000000000000000000000000..bd51ef92de73122646d92db194805137427119d5
--- /dev/null
+++ b/webook/search/repository/any.go
@@ -0,0 +1,22 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+)
+
+type AnyRepository interface {
+ Input(ctx context.Context, index string, docID string, data string) error
+}
+
+type anyRepository struct {
+ dao dao.AnyDAO
+}
+
+func NewAnyRepository(dao dao.AnyDAO) AnyRepository {
+ return &anyRepository{dao: dao}
+}
+
+func (repo *anyRepository) Input(ctx context.Context, index string, docID string, data string) error {
+ return repo.dao.Input(ctx, index, docID, data)
+}
diff --git a/webook/search/repository/article.go b/webook/search/repository/article.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cad48eb406ff29b904cfd22635fba53f7807266
--- /dev/null
+++ b/webook/search/repository/article.go
@@ -0,0 +1,53 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "github.com/ecodeclub/ekit/slice"
+)
+
+type articleRepository struct {
+ dao dao.ArticleDAO
+ tags dao.TagDAO
+}
+
+func (a *articleRepository) SearchArticle(ctx context.Context,
+ uid int64,
+ keywords []string) ([]domain.Article, error) {
+ // 标签命中了的
+ ids, err := a.tags.Search(ctx, uid, "article", keywords)
+ if err != nil {
+ return nil, err
+ }
+ // 加一个 bizids 的输入,这个 bizid 是标签含有关键字的 biz_id
+ arts, err := a.dao.Search(ctx, ids, keywords)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(arts, func(idx int, src dao.Article) domain.Article {
+ return domain.Article{
+ Id: src.Id,
+ Title: src.Title,
+ Status: src.Status,
+ Content: src.Content,
+ Tags: src.Tags,
+ }
+ }), nil
+}
+
+func (a *articleRepository) InputArticle(ctx context.Context, msg domain.Article) error {
+ return a.dao.InputArticle(ctx, dao.Article{
+ Id: msg.Id,
+ Title: msg.Title,
+ Status: msg.Status,
+ Content: msg.Content,
+ })
+}
+
+func NewArticleRepository(d dao.ArticleDAO, td dao.TagDAO) ArticleRepository {
+ return &articleRepository{
+ dao: d,
+ tags: td,
+ }
+}
diff --git a/webook/search/repository/dao/any_es.go b/webook/search/repository/dao/any_es.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd99b4b727f55d72d7df17d72922a64ad0a0e184
--- /dev/null
+++ b/webook/search/repository/dao/any_es.go
@@ -0,0 +1,21 @@
+package dao
+
+import (
+ "context"
+ "github.com/olivere/elastic/v7"
+)
+
+type AnyESDAO struct {
+ client *elastic.Client
+}
+
+func NewAnyESDAO(client *elastic.Client) AnyDAO {
+ return &AnyESDAO{client: client}
+}
+
+func (a *AnyESDAO) Input(ctx context.Context, index, docId, data string) error {
+ _, err := a.client.Index().
+ // 直接整个 data 从 Kafka/grpc 里面一路透传到这里
+ Index(index).Id(docId).BodyString(data).Do(ctx)
+ return err
+}
diff --git a/webook/search/repository/dao/article_es.go b/webook/search/repository/dao/article_es.go
new file mode 100644
index 0000000000000000000000000000000000000000..83eb2ab2f7ac52197e46bb63623a6c3898d2827f
--- /dev/null
+++ b/webook/search/repository/dao/article_es.go
@@ -0,0 +1,128 @@
+package dao
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/ecodeclub/ekit/slice"
+ "github.com/olivere/elastic/v7"
+ "strconv"
+ "strings"
+)
+
+const ArticleIndexName = "article_index"
+const TagIndexName = "tags_index"
+
+type Article struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Status int32 `json:"status"`
+ Content string `json:"content"`
+ Tags []string `json:"tags"`
+}
+
+type ArticleElasticDAO struct {
+ client *elastic.Client
+}
+
+func NewArticleElasticDAO(client *elastic.Client) ArticleDAO {
+ return &ArticleElasticDAO{client: client}
+}
+
+func (h *ArticleElasticDAO) Search(ctx context.Context, tagArtIds []int64, keywords []string) ([]Article, error) {
+ queryString := strings.Join(keywords, " ")
+ // 文章,标题或者内容任何一个匹配上
+ // 并且状态 status 必须是已发表的状态
+
+ // status 精确查找
+ statusTerm := elastic.NewTermQuery("status", 2)
+
+ // 标签命中
+ tagArtIdAnys := slice.Map(tagArtIds, func(idx int, src int64) any {
+ return src
+ })
+
+ // 内容或者标题,模糊查找(match)
+ title := elastic.NewMatchQuery("title", queryString)
+ content := elastic.NewMatchQuery("content", queryString)
+ or := elastic.NewBoolQuery().Should(title, content)
+ if len(tagArtIds) > 0 {
+ tag := elastic.NewTermsQuery("id", tagArtIdAnys...).
+ Boost(2.0)
+ or = or.Should(tag)
+ }
+
+ and := elastic.NewBoolQuery().Must(statusTerm, or)
+
+ //return NewSearcher[Article](h.client, ArticleIndexName).
+ // Query(and).Do(ctx)
+ resp, err := h.client.Search(ArticleIndexName).Query(and).Do(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var res []Article
+ for _, hit := range resp.Hits.Hits {
+ var art Article
+ err = json.Unmarshal(hit.Source, &art)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, art)
+ }
+ return res, nil
+}
+
+func (h *ArticleElasticDAO) InputArticle(ctx context.Context, art Article) error {
+ _, err := h.client.Index().Index(ArticleIndexName).
+ // 为什么要指定 ID?
+ // 确保后面文章更新的时候,我们这里产生类似的两条数据,而是更新了数据
+ Id(strconv.FormatInt(art.Id, 10)).
+ BodyJson(art).Do(ctx)
+ return err
+}
+
+func NewArticleRepository(client *elastic.Client) ArticleDAO {
+ return &ArticleElasticDAO{
+ client: client,
+ }
+}
+
+type Searcher[T any] struct {
+ client *elastic.Client
+ idxName []string
+ query elastic.Query
+}
+
+func NewSearcher[T any](client *elastic.Client, idxName ...string) *Searcher[T] {
+ return &Searcher[T]{
+ client: client,
+ idxName: idxName,
+ }
+}
+
+func (s *Searcher[T]) Query(q elastic.Query) *Searcher[T] {
+ s.query = q
+ return s
+}
+
+//
+//func (s *Searcher[T]) Do1(ctx context.Context) (T, error) {
+//
+//}
+
+func (s *Searcher[T]) Do(ctx context.Context) ([]T, error) {
+ resp, err := s.client.Search(s.idxName...).Do(ctx)
+ res := make([]T, 0, resp.Hits.TotalHits.Value)
+ for _, hit := range resp.Hits.Hits {
+ var t T
+ err = json.Unmarshal(hit.Source, &t)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, t)
+ }
+ return res, nil
+}
+
+//func (s *Searcher[T]) Resp() *elastic.SearchResult {
+//
+//}
diff --git a/webook/search/repository/dao/article_index.json b/webook/search/repository/dao/article_index.json
new file mode 100644
index 0000000000000000000000000000000000000000..b9f9ae3942501271a1b5652f9c9dd53043127451
--- /dev/null
+++ b/webook/search/repository/dao/article_index.json
@@ -0,0 +1,16 @@
+{
+ "mapping": {
+ "id": {
+ "type": "long"
+ },
+ "title": {
+ "type": "text"
+ },
+ "content": {
+ "type": "text"
+ },
+ "status": {
+ "type": "integer"
+ }
+ }
+}
\ No newline at end of file
diff --git a/webook/search/repository/dao/init_index.go b/webook/search/repository/dao/init_index.go
new file mode 100644
index 0000000000000000000000000000000000000000..c400b9ece876e38d6995d982de7082bc4524d276
--- /dev/null
+++ b/webook/search/repository/dao/init_index.go
@@ -0,0 +1,52 @@
+package dao
+
+import (
+ "context"
+ _ "embed"
+ "github.com/olivere/elastic/v7"
+ "golang.org/x/sync/errgroup"
+ "time"
+)
+
+var (
+ //go:embed user_index.json
+ userIndex string
+ //go:embed article_index.json
+ articleIndex string
+ //go:embed tags_index.json
+ tagIndex string
+)
+
+// InitES 创建索引
+func InitES(client *elastic.Client) error {
+ const timeout = time.Second * 10
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ var eg errgroup.Group
+ eg.Go(func() error {
+ return tryCreateIndex(ctx, client, UserIndexName, userIndex)
+ })
+ eg.Go(func() error {
+ return tryCreateIndex(ctx, client, ArticleIndexName, articleIndex)
+ })
+ eg.Go(func() error { return tryCreateIndex(ctx, client, TagIndexName, tagIndex) })
+
+ return eg.Wait()
+}
+
+func tryCreateIndex(ctx context.Context,
+ client *elastic.Client,
+ idxName, idxCfg string,
+) error {
+ // 你要考虑,这个索引可能已经建好了
+ // 有并发问题的
+ ok, err := client.IndexExists(idxName).Do(ctx)
+ if err != nil {
+ return err
+ }
+ if ok {
+ return nil
+ }
+ _, err = client.CreateIndex(idxName).Body(idxCfg).Do(ctx)
+ return err
+}
diff --git a/webook/search/repository/dao/tag_es.go b/webook/search/repository/dao/tag_es.go
new file mode 100644
index 0000000000000000000000000000000000000000..5be07a0e5ecc730eec86f21a69dc3ea16d1356f4
--- /dev/null
+++ b/webook/search/repository/dao/tag_es.go
@@ -0,0 +1,48 @@
+package dao
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/olivere/elastic/v7"
+)
+
+type TagESDAO struct {
+ client *elastic.Client
+}
+
+func NewTagESDAO(client *elastic.Client) TagDAO {
+ return &TagESDAO{client: client}
+}
+
+func (t *TagESDAO) Search(ctx context.Context, uid int64, biz string, keywords []string) ([]int64, error) {
+ query := elastic.NewBoolQuery().Must(
+ // 第一个条件,一定有 uid
+ elastic.NewTermQuery("uid", uid),
+ // 第二个条件,biz 一定符合预期
+ elastic.NewTermQuery("biz", biz),
+ // 第三个条件,关键字命中了标签
+ elastic.NewTermsQueryFromStrings("tags", keywords...),
+ )
+ resp, err := t.client.Search(TagIndexName).Query(query).Do(ctx)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]int64, 0, len(resp.Hits.Hits))
+ for _, hit := range resp.Hits.Hits {
+ var ele BizTags
+ err = json.Unmarshal(hit.Source, &ele)
+ if err != nil {
+ return nil, err
+ }
+ // 把 biz_id 拿出来了
+ res = append(res, ele.BizId)
+ }
+ return res, nil
+}
+
+type BizTags struct {
+ Uid int64 `json:"uid"`
+ Biz string `json:"biz"`
+ BizId int64 `json:"biz_id"`
+ Tags []string `json:"tags"`
+}
diff --git a/webook/search/repository/dao/tags_index.json b/webook/search/repository/dao/tags_index.json
new file mode 100644
index 0000000000000000000000000000000000000000..910e475f5080e22483dea845b91cc2c61a787628
--- /dev/null
+++ b/webook/search/repository/dao/tags_index.json
@@ -0,0 +1,18 @@
+{
+ "mappings": {
+ "properties": {
+ "tags": {
+ "type": "keyword"
+ },
+ "uid": {
+ "type": "long"
+ },
+ "biz": {
+ "type": "keyword"
+ },
+ "biz_id": {
+ "type": "long"
+ }
+ }
+ }
+}
diff --git a/webook/search/repository/dao/type.go b/webook/search/repository/dao/type.go
new file mode 100644
index 0000000000000000000000000000000000000000..6258b3f53c734908483e25c42684fe886d205d91
--- /dev/null
+++ b/webook/search/repository/dao/type.go
@@ -0,0 +1,23 @@
+package dao
+
+import (
+ "context"
+)
+
+type UserDAO interface {
+ InputUser(ctx context.Context, user User) error
+ Search(ctx context.Context, keywords []string) ([]User, error)
+}
+
+type ArticleDAO interface {
+ InputArticle(ctx context.Context, article Article) error
+ Search(ctx context.Context, tagArtIds []int64, keywords []string) ([]Article, error)
+}
+
+type TagDAO interface {
+ Search(ctx context.Context, uid int64, biz string, keywords []string) ([]int64, error)
+}
+
+type AnyDAO interface {
+ Input(ctx context.Context, index, docID, data string) error
+}
diff --git a/webook/search/repository/dao/user_es.go b/webook/search/repository/dao/user_es.go
new file mode 100644
index 0000000000000000000000000000000000000000..3f826a14f677d48eb9162a384b3698a41acd61a8
--- /dev/null
+++ b/webook/search/repository/dao/user_es.go
@@ -0,0 +1,57 @@
+package dao
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/olivere/elastic/v7"
+ "strconv"
+ "strings"
+)
+
+const UserIndexName = "user_index"
+
+type User struct {
+ Id int64 `json:"id"`
+ Email string `json:"email"`
+ Nickname string `json:"nickname"`
+ Phone string `json:"phone"`
+}
+
+type UserElasticDAO struct {
+ client *elastic.Client
+}
+
+func (h *UserElasticDAO) Search(ctx context.Context, keywords []string) ([]User, error) {
+ // 纯粹是因为前面我们已经预处理了输入
+ queryString := strings.Join(keywords, " ")
+ // 昵称命中就可以的
+ resp, err := h.client.Search(UserIndexName).
+ Query(elastic.NewMatchQuery("nickname", queryString)).Do(ctx)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]User, 0, resp.Hits.TotalHits.Value)
+ for _, hit := range resp.Hits.Hits {
+ var u User
+ err = json.Unmarshal(hit.Source, &u)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, u)
+ }
+ return res, nil
+}
+
+func (h *UserElasticDAO) InputUser(ctx context.Context, user User) error {
+ _, err := h.client.Index().
+ Index(UserIndexName).
+ Id(strconv.FormatInt(user.Id, 10)).
+ BodyJson(user).Do(ctx)
+ return err
+}
+
+func NewUserElasticDAO(client *elastic.Client) UserDAO {
+ return &UserElasticDAO{
+ client: client,
+ }
+}
diff --git a/webook/search/repository/dao/user_index.json b/webook/search/repository/dao/user_index.json
new file mode 100644
index 0000000000000000000000000000000000000000..cb4607a68fa89a23c14e1840a8405bb1e24a98e3
--- /dev/null
+++ b/webook/search/repository/dao/user_index.json
@@ -0,0 +1,16 @@
+{
+ "mapping": {
+ "id": {
+ "type": "long"
+ },
+ "nickname": {
+ "type": "text"
+ },
+ "email": {
+ "type": "text"
+ },
+ "phone": {
+ "type": "keyword"
+ }
+ }
+}
\ No newline at end of file
diff --git a/webook/search/repository/type.go b/webook/search/repository/type.go
new file mode 100644
index 0000000000000000000000000000000000000000..877dd7c9321c37f696ffa86df223e4b0b9adf834
--- /dev/null
+++ b/webook/search/repository/type.go
@@ -0,0 +1,16 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+)
+
+type UserRepository interface {
+ InputUser(ctx context.Context, msg domain.User) error
+ SearchUser(ctx context.Context, keywords []string) ([]domain.User, error)
+}
+
+type ArticleRepository interface {
+ InputArticle(ctx context.Context, msg domain.Article) error
+ SearchArticle(ctx context.Context, uid int64, keywords []string) ([]domain.Article, error)
+}
diff --git a/webook/search/repository/user.go b/webook/search/repository/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..caa6c2cb924af118d7978b07ff167fc165fe89d3
--- /dev/null
+++ b/webook/search/repository/user.go
@@ -0,0 +1,43 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "github.com/ecodeclub/ekit/slice"
+)
+
+type userRepository struct {
+ dao dao.UserDAO
+}
+
+func (u *userRepository) SearchUser(ctx context.Context, keywords []string) ([]domain.User, error) {
+ users, err := u.dao.Search(ctx, keywords)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(users, func(idx int, src dao.User) domain.User {
+ return domain.User{
+ Id: src.Id,
+ Email: src.Email,
+ Nickname: src.Nickname,
+ Phone: src.Phone,
+ }
+ }), nil
+}
+
+func (u *userRepository) InputUser(ctx context.Context, msg domain.User) error {
+ return u.dao.InputUser(ctx, dao.User{
+ Id: msg.Id,
+ Email: msg.Email,
+ Nickname: msg.Nickname,
+ Phone: msg.Phone,
+ })
+
+}
+
+func NewUserRepository(d dao.UserDAO) UserRepository {
+ return &userRepository{
+ dao: d,
+ }
+}
diff --git a/webook/search/service/search.go b/webook/search/service/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..d85b31444f056f5b9af052f4d3b76bd5652a59e7
--- /dev/null
+++ b/webook/search/service/search.go
@@ -0,0 +1,46 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+ "golang.org/x/sync/errgroup"
+ "strings"
+)
+
+type SearchService interface {
+ Search(ctx context.Context, uid int64, expression string) (domain.SearchResult, error)
+}
+
+type searchService struct {
+ userRepo repository.UserRepository
+ articleRepo repository.ArticleRepository
+}
+
+func NewSearchService(userRepo repository.UserRepository, articleRepo repository.ArticleRepository) SearchService {
+ return &searchService{userRepo: userRepo, articleRepo: articleRepo}
+}
+
+func (s *searchService) Search(ctx context.Context, uid int64, expression string) (domain.SearchResult, error) {
+ // 这边一般要对 expression 进行一些预处理
+ // 正常大家都是使用的空格符来分割的,但是有些时候可能会手抖,输错
+ keywords := strings.Split(expression, " ")
+ // 注意这里我们没有使用 multi query 或者 multi match 之类的写法
+ // 是因为正常来说,不同的业务放过来的数据,什么支持搜索,什么不支持搜索,
+ // 以及究竟怎么用于搜索,都是有区别的。所以这里我们利用两个 repo 来组合结果
+ var eg errgroup.Group
+ var res domain.SearchResult
+ eg.Go(func() error {
+ users, err := s.userRepo.SearchUser(ctx, keywords)
+ res.Users = users
+ return err
+ })
+ eg.Go(func() error {
+ // 0000 0011
+ arts, err := s.articleRepo.SearchArticle(ctx, uid, keywords)
+ res.Articles = arts
+ return err
+ })
+ // 如果你有更多,你就在这里继续开 eg.Go
+ return res, eg.Wait()
+}
diff --git a/webook/search/service/sync.go b/webook/search/service/sync.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ee8abdb9f8dcfc67c233daa97129700834b390b
--- /dev/null
+++ b/webook/search/service/sync.go
@@ -0,0 +1,44 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/search/domain"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+)
+
+type SyncService interface {
+ InputArticle(ctx context.Context, article domain.Article) error
+ InputUser(ctx context.Context, user domain.User) error
+ InputAny(ctx context.Context, index, docID, data string) error
+}
+
+type syncService struct {
+ userRepo repository.UserRepository
+ articleRepo repository.ArticleRepository
+ anyRepo repository.AnyRepository
+}
+
+func (s *syncService) InputAny(ctx context.Context, index, docID, data string) error {
+ //cvt := s.converter(index)
+ //data = cvt.Convert(data)
+ return s.anyRepo.Input(ctx, index, docID, data)
+}
+
+func (s *syncService) InputArticle(ctx context.Context, article domain.Article) error {
+ return s.articleRepo.InputArticle(ctx, article)
+}
+
+func (s *syncService) InputUser(ctx context.Context, user domain.User) error {
+ return s.userRepo.InputUser(ctx, user)
+}
+
+func NewSyncService(
+ anyRepo repository.AnyRepository,
+ userRepo repository.UserRepository,
+ articleRepo repository.ArticleRepository) SyncService {
+ return &syncService{
+ userRepo: userRepo,
+ articleRepo: articleRepo,
+ anyRepo: anyRepo,
+ }
+}
diff --git a/webook/search/wire.go b/webook/search/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..9945954f8ed3e28ad6734c8feecff6b7cdc0b111
--- /dev/null
+++ b/webook/search/wire.go
@@ -0,0 +1,46 @@
+//go:build wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/events"
+ "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "gitee.com/geekbang/basic-go/webook/search/ioc"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/google/wire"
+)
+
+var serviceProviderSet = wire.NewSet(
+ dao.NewUserElasticDAO,
+ dao.NewArticleElasticDAO,
+ dao.NewAnyESDAO,
+ dao.NewTagESDAO,
+ repository.NewUserRepository,
+ repository.NewArticleRepository,
+ repository.NewAnyRepository,
+ service.NewSyncService,
+ service.NewSearchService,
+)
+
+var thirdProvider = wire.NewSet(
+ ioc.InitESClient,
+ ioc.InitEtcdClient,
+ ioc.InitLogger,
+ ioc.InitKafka)
+
+func Init() *App {
+ wire.Build(
+ thirdProvider,
+ serviceProviderSet,
+ grpc.NewSyncServiceServer,
+ grpc.NewSearchService,
+ events.NewUserConsumer,
+ events.NewArticleConsumer,
+ ioc.InitGRPCxServer,
+ ioc.NewConsumers,
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/search/wire_gen.go b/webook/search/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc032a760204e560f1af007cf9df1ac1a6e469aa
--- /dev/null
+++ b/webook/search/wire_gen.go
@@ -0,0 +1,52 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/search/events"
+ "gitee.com/geekbang/basic-go/webook/search/grpc"
+ "gitee.com/geekbang/basic-go/webook/search/ioc"
+ "gitee.com/geekbang/basic-go/webook/search/repository"
+ "gitee.com/geekbang/basic-go/webook/search/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/google/wire"
+)
+
+// Injectors from wire.go:
+
+func Init() *App {
+ client := ioc.InitESClient()
+ anyDAO := dao.NewAnyESDAO(client)
+ anyRepository := repository.NewAnyRepository(anyDAO)
+ userDAO := dao.NewUserElasticDAO(client)
+ userRepository := repository.NewUserRepository(userDAO)
+ articleDAO := dao.NewArticleElasticDAO(client)
+ tagDAO := dao.NewTagESDAO(client)
+ articleRepository := repository.NewArticleRepository(articleDAO, tagDAO)
+ syncService := service.NewSyncService(anyRepository, userRepository, articleRepository)
+ syncServiceServer := grpc.NewSyncServiceServer(syncService)
+ searchService := service.NewSearchService(userRepository, articleRepository)
+ searchServiceServer := grpc.NewSearchService(searchService)
+ clientv3Client := ioc.InitEtcdClient()
+ loggerV1 := ioc.InitLogger()
+ server := ioc.InitGRPCxServer(syncServiceServer, searchServiceServer, clientv3Client, loggerV1)
+ saramaClient := ioc.InitKafka()
+ articleConsumer := events.NewArticleConsumer(saramaClient, loggerV1, syncService)
+ userConsumer := events.NewUserConsumer(saramaClient, loggerV1, syncService)
+ v := ioc.NewConsumers(articleConsumer, userConsumer)
+ app := &App{
+ server: server,
+ consumers: v,
+ }
+ return app
+}
+
+// wire.go:
+
+var serviceProviderSet = wire.NewSet(dao.NewUserElasticDAO, dao.NewArticleElasticDAO, dao.NewAnyESDAO, dao.NewTagESDAO, repository.NewUserRepository, repository.NewArticleRepository, repository.NewAnyRepository, service.NewSyncService, service.NewSearchService)
+
+var thirdProvider = wire.NewSet(ioc.InitESClient, ioc.InitEtcdClient, ioc.InitLogger, ioc.InitKafka)
diff --git a/webook/tag/config/dev.yaml b/webook/tag/config/dev.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..31756b638fb479c1ee307c86206acca1b65c0dab
--- /dev/null
+++ b/webook/tag/config/dev.yaml
@@ -0,0 +1,13 @@
+db:
+ dsn: "root:root@tcp(localhost:13316)/webook_article"
+
+redis:
+ addr: "localhost:6379"
+
+grpc:
+ server:
+ # 启动监听 8090 端口,你如果启动很多个服务的话,小心端口冲突
+ addr: ":8097"
+ client:
+ user:
+ addr: ":8091"
\ No newline at end of file
diff --git a/webook/tag/domain/tag.go b/webook/tag/domain/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..b77fc76dc1e6b93415afe7d6c14082bf63b2150f
--- /dev/null
+++ b/webook/tag/domain/tag.go
@@ -0,0 +1,7 @@
+package domain
+
+type Tag struct {
+ Id int64
+ Name string
+ Uid int64
+}
diff --git a/webook/tag/errs/code.go b/webook/tag/errs/code.go
new file mode 100644
index 0000000000000000000000000000000000000000..b1e433e131c6bfb985ef7a49a00680154d8364d5
--- /dev/null
+++ b/webook/tag/errs/code.go
@@ -0,0 +1,4 @@
+package errs
+
+// Tag 部分,模块代码使用 02
+const ()
diff --git a/webook/tag/events/producer.go b/webook/tag/events/producer.go
new file mode 100644
index 0000000000000000000000000000000000000000..c344a16123a91b162be5c87d8da566150392393f
--- /dev/null
+++ b/webook/tag/events/producer.go
@@ -0,0 +1,41 @@
+package events
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/IBM/sarama"
+)
+
+type Producer interface {
+ ProduceSyncEvent(ctx context.Context, data BizTags) error
+}
+
+type SaramaSyncProducer struct {
+ client sarama.SyncProducer
+}
+
+func (p *SaramaSyncProducer) ProduceSyncEvent(ctx context.Context, tags BizTags) error {
+ data, _ := json.Marshal(tags)
+ evt := SyncDataEvent{
+ IndexName: "tags_index",
+ // 构成一个唯一的 doc id
+ // 要确保后面打了新标签的时候,搜索那边也会有对应的修改
+ DocID: fmt.Sprintf("%d_%s_%d", tags.Uid, tags.Biz, tags.BizId),
+ Data: string(data),
+ }
+ data, _ = json.Marshal(evt)
+ _, _, err := p.client.SendMessage(&sarama.ProducerMessage{
+ Topic: "search_sync_data",
+ Value: sarama.ByteEncoder(data),
+ })
+ return err
+}
+
+type BizTags struct {
+ Uid int64 `json:"uid"`
+ Biz string `json:"biz"`
+ BizId int64 `json:"biz_id"`
+ // 只传递 string
+ Tags []string `json:"tags"`
+}
diff --git a/webook/tag/events/search.go b/webook/tag/events/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..c74c305b4761a7e0f08147500a2289da8206a669
--- /dev/null
+++ b/webook/tag/events/search.go
@@ -0,0 +1,19 @@
+package events
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/search/service"
+ "github.com/IBM/sarama"
+)
+
+type SyncDataEvent struct {
+ IndexName string
+ DocID string
+ Data string
+}
+
+type SyncDataEventConsumer struct {
+ svc service.SyncService
+ client sarama.Client
+ l logger.LoggerV1
+}
diff --git a/webook/tag/grpc/tag.go b/webook/tag/grpc/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee1793febe0e44d008ea8c8cfe9833faa2354991
--- /dev/null
+++ b/webook/tag/grpc/tag.go
@@ -0,0 +1,75 @@
+package grpc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/api/proto/gen/tag/v1"
+ "gitee.com/geekbang/basic-go/webook/tag/domain"
+ "gitee.com/geekbang/basic-go/webook/tag/service"
+ "github.com/ecodeclub/ekit/slice"
+ "google.golang.org/grpc"
+)
+
+var _ tagv1.TagServiceServer = (*TagServiceServer)(nil)
+
+type TagServiceServer struct {
+ tagv1.UnimplementedTagServiceServer
+ service service.TagService
+}
+
+func (t *TagServiceServer) Register(server grpc.ServiceRegistrar) {
+ tagv1.RegisterTagServiceServer(server, t)
+}
+
+func (t *TagServiceServer) CreateTag(ctx context.Context, request *tagv1.CreateTagRequest) (*tagv1.CreateTagResponse, error) {
+ id, err := t.service.CreateTag(ctx, request.Uid, request.Name)
+ return &tagv1.CreateTagResponse{
+ Tag: &tagv1.Tag{
+ Id: id,
+ Uid: request.Uid,
+ Name: request.Name,
+ },
+ }, err
+}
+
+func (t *TagServiceServer) AttachTags(ctx context.Context, request *tagv1.AttachTagsRequest) (*tagv1.AttachTagsResponse, error) {
+ err := t.service.AttachTags(ctx, request.Uid, request.Biz, request.BizId, request.Tids)
+ return &tagv1.AttachTagsResponse{}, err
+}
+
+func (t *TagServiceServer) GetTags(ctx context.Context, request *tagv1.GetTagsRequest) (*tagv1.GetTagsResponse, error) {
+ tags, err := t.service.GetTags(ctx, request.GetUid())
+ if err != nil {
+ return nil, err
+ }
+ return &tagv1.GetTagsResponse{
+ Tag: slice.Map(tags, func(idx int, src domain.Tag) *tagv1.Tag {
+ return t.toDTO(src)
+ }),
+ }, nil
+}
+
+func (t *TagServiceServer) GetBizTags(ctx context.Context, req *tagv1.GetBizTagsRequest) (*tagv1.GetBizTagsResponse, error) {
+ res, err := t.service.GetBizTags(ctx, req.Uid, req.Biz, req.BizId)
+ if err != nil {
+ return nil, err
+ }
+ return &tagv1.GetBizTagsResponse{
+ Tags: slice.Map(res, func(idx int, src domain.Tag) *tagv1.Tag {
+ return t.toDTO(src)
+ }),
+ }, nil
+}
+
+func (t *TagServiceServer) toDTO(tag domain.Tag) *tagv1.Tag {
+ return &tagv1.Tag{
+ Id: tag.Id,
+ Uid: tag.Uid,
+ Name: tag.Name,
+ }
+}
+
+func NewTagServiceServer(svc service.TagService) *TagServiceServer {
+ return &TagServiceServer{
+ service: svc,
+ }
+}
diff --git a/webook/tag/integration/startup/db.go b/webook/tag/integration/startup/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..23c7e2f0d306624b044f0f2931b9e1794965f26e
--- /dev/null
+++ b/webook/tag/integration/startup/db.go
@@ -0,0 +1,43 @@
+package startup
+
+import (
+ "context"
+ "database/sql"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "log"
+ "time"
+)
+
+var db *gorm.DB
+
+// InitTestDB 测试的话,不用控制并发。等遇到了并发问题再说
+func InitTestDB() *gorm.DB {
+ if db == nil {
+ dsn := "root:root@tcp(localhost:13316)/webook_tag"
+ sqlDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ panic(err)
+ }
+ for {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = sqlDB.PingContext(ctx)
+ cancel()
+ if err == nil {
+ break
+ }
+ log.Println("等待连接 MySQL", err)
+ }
+ db, err = gorm.Open(mysql.Open(dsn))
+ if err != nil {
+ panic(err)
+ }
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ //db = db.Debug()
+ }
+ return db
+}
diff --git a/webook/tag/integration/startup/log.go b/webook/tag/integration/startup/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..a659ad9dbf326536df6bc5e6641a4aed105b15bc
--- /dev/null
+++ b/webook/tag/integration/startup/log.go
@@ -0,0 +1,9 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+)
+
+func InitLog() logger.LoggerV1 {
+ return logger.NewNoOpLogger()
+}
diff --git a/webook/tag/integration/startup/redis.go b/webook/tag/integration/startup/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4d929d011e0aa42e75aa0c6c68fb582c39c8db7
--- /dev/null
+++ b/webook/tag/integration/startup/redis.go
@@ -0,0 +1,21 @@
+package startup
+
+import (
+ "context"
+ "github.com/redis/go-redis/v9"
+)
+
+var redisClient redis.Cmdable
+
+func InitRedis() redis.Cmdable {
+ if redisClient == nil {
+ redisClient = redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ })
+
+ for err := redisClient.Ping(context.Background()).Err(); err != nil; {
+ panic(err)
+ }
+ }
+ return redisClient
+}
diff --git a/webook/tag/integration/startup/repository.go b/webook/tag/integration/startup/repository.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf989d6d502e26e4558aabf5da1a9778f6495447
--- /dev/null
+++ b/webook/tag/integration/startup/repository.go
@@ -0,0 +1,12 @@
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/tag/repository"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+)
+
+func InitRepository(d dao.TagDAO, c cache.TagCache, l logger.LoggerV1) repository.TagRepository {
+ return repository.NewTagRepository(d, c, l)
+}
diff --git a/webook/tag/integration/startup/wire.go b/webook/tag/integration/startup/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..6664cf50aa5e80bb457ed2fac69f1478d6179a01
--- /dev/null
+++ b/webook/tag/integration/startup/wire.go
@@ -0,0 +1,23 @@
+//go:build wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/tag/grpc"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/tag/service"
+ "github.com/google/wire"
+)
+
+func InitGRPCService() *grpc.TagServiceServer {
+ wire.Build(InitTestDB, InitRedis,
+ InitLog,
+ dao.NewGORMTagDAO,
+ InitRepository,
+ cache.NewRedisTagCache,
+ service.NewTagService,
+ grpc.NewTagServiceServer,
+ )
+ return new(grpc.TagServiceServer)
+}
diff --git a/webook/tag/integration/startup/wire_gen.go b/webook/tag/integration/startup/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..1cfede720832526711341ea5283fc05f1e11f319
--- /dev/null
+++ b/webook/tag/integration/startup/wire_gen.go
@@ -0,0 +1,28 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package startup
+
+import (
+ "gitee.com/geekbang/basic-go/webook/tag/grpc"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/tag/service"
+)
+
+// Injectors from wire.go:
+
+func InitGRPCService() *grpc.TagServiceServer {
+ gormDB := InitTestDB()
+ tagDAO := dao.NewGORMTagDAO(gormDB)
+ cmdable := InitRedis()
+ tagCache := cache.NewRedisTagCache(cmdable)
+ loggerV1 := InitLog()
+ tagRepository := InitRepository(tagDAO, tagCache, loggerV1)
+ tagService := service.NewTagService(tagRepository, loggerV1)
+ tagServiceServer := grpc.NewTagServiceServer(tagService)
+ return tagServiceServer
+}
diff --git a/webook/tag/integration/tag_service_test.go b/webook/tag/integration/tag_service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..aaf291ed76abc8a24b9c40f6ee52441f9846b65e
--- /dev/null
+++ b/webook/tag/integration/tag_service_test.go
@@ -0,0 +1,63 @@
+package integration
+
+import (
+ "context"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/tag/grpc"
+ "gitee.com/geekbang/basic-go/webook/tag/integration/startup"
+ "gitee.com/geekbang/basic-go/webook/tag/repository"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "gorm.io/gorm"
+ "testing"
+ "time"
+)
+
+type TagServiceTestSuite struct {
+ suite.Suite
+ svc *grpc.TagServiceServer
+ db *gorm.DB
+ rdb redis.Cmdable
+}
+
+func (s *TagServiceTestSuite) SetupSuite() {
+ s.svc = startup.InitGRPCService()
+ s.db = startup.InitTestDB()
+ s.rdb = startup.InitRedis()
+}
+
+func (s *TagServiceTestSuite) TearDownSuite() {
+ err := s.db.Exec("TRUNCATE TABLE `tag_bizs`").Error
+ require.NoError(s.T(), err)
+ // 在有外键约束的情况下,不能用 TRUNCATE
+ err = s.db.Exec("DELETE FROM `tags`").Error
+ require.NoError(s.T(), err)
+}
+
+func TestTagService(t *testing.T) {
+ suite.Run(t, new(TagServiceTestSuite))
+}
+
+func (s *TagServiceTestSuite) TestPreload() {
+ data := make([]dao.Tag, 0, 200)
+ for i := 0; i < 200; i++ {
+ data = append(data, dao.Tag{
+ Id: int64(i + 1),
+ Name: fmt.Sprintf("tag_%d", i),
+ Uid: int64(i+1) % 3,
+ })
+ }
+ err := s.db.Create(&data).Error
+ require.NoError(s.T(), err)
+ d := dao.NewGORMTagDAO(s.db)
+ c := cache.NewRedisTagCache(s.rdb)
+ l := startup.InitLog()
+ repo := repository.NewTagRepository(d, c, l)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
+ defer cancel()
+ err = repo.PreloadUserTags(ctx)
+ require.NoError(s.T(), err)
+}
diff --git a/webook/tag/ioc/db.go b/webook/tag/ioc/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..bfeb08bd1b077a486bfb13496510ba965ad06bd2
--- /dev/null
+++ b/webook/tag/ioc/db.go
@@ -0,0 +1,67 @@
+package ioc
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "github.com/spf13/viper"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "gorm.io/plugin/opentelemetry/tracing"
+ "gorm.io/plugin/prometheus"
+)
+
+func InitDB(l logger.LoggerV1) *gorm.DB {
+ type Config struct {
+ DSN string `yaml:"dsn"`
+ }
+ c := Config{
+ DSN: "root:root@tcp(localhost:3306)/mysql",
+ }
+ err := viper.UnmarshalKey("db", &c)
+ if err != nil {
+ panic(fmt.Errorf("初始化配置失败 %v1, 原因 %w", c, err))
+ }
+ db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{
+ // 使用 DEBUG 来打印
+ //Logger: glogger.New(gormLoggerFunc(l.Debug),
+ // glogger.Config{
+ // SlowThreshold: 0,
+ // LogLevel: glogger.Info,
+ // }),
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // 接入 prometheus
+ err = db.Use(prometheus.New(prometheus.Config{
+ DBName: "webook",
+ // 每 15 秒采集一些数据
+ RefreshInterval: 15,
+ MetricsCollector: []prometheus.MetricsCollector{
+ &prometheus.MySQL{
+ VariableNames: []string{"Threads_running"},
+ },
+ }, // user defined metrics
+ }))
+ if err != nil {
+ panic(err)
+ }
+ err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics()))
+ if err != nil {
+ panic(err)
+ }
+
+ err = dao.InitTables(db)
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
+
+type gormLoggerFunc func(msg string, fields ...logger.Field)
+
+func (g gormLoggerFunc) Printf(msg string, args ...interface{}) {
+ g(msg, logger.Field{Key: "args", Value: args})
+}
diff --git a/webook/tag/ioc/grpc.go b/webook/tag/ioc/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ba59c9e21b23c8a186953643f9bb3beae3e982c
--- /dev/null
+++ b/webook/tag/ioc/grpc.go
@@ -0,0 +1,35 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/grpcx"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ grpc2 "gitee.com/geekbang/basic-go/webook/tag/grpc"
+ "github.com/spf13/viper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "google.golang.org/grpc"
+)
+
+func InitGRPCxServer(asc *grpc2.TagServiceServer,
+ ecli *clientv3.Client,
+ l logger.LoggerV1) *grpcx.Server {
+ type Config struct {
+ Port int `yaml:"port"`
+ EtcdAddr string `yaml:"etcdAddr"`
+ EtcdTTL int64 `yaml:"etcdTTL"`
+ }
+ var cfg Config
+ err := viper.UnmarshalKey("grpc.server", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ server := grpc.NewServer()
+ asc.Register(server)
+ return &grpcx.Server{
+ Server: server,
+ Port: cfg.Port,
+ Name: "reward",
+ L: l,
+ EtcdClient: ecli,
+ EtcdTTL: cfg.EtcdTTL,
+ }
+}
diff --git a/webook/tag/ioc/log.go b/webook/tag/ioc/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..82c309b0ea39200912d0129fd3ead9bc95f674bc
--- /dev/null
+++ b/webook/tag/ioc/log.go
@@ -0,0 +1,22 @@
+package ioc
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "github.com/spf13/viper"
+ "go.uber.org/zap"
+)
+
+func InitLogger() logger.LoggerV1 {
+ // 这里我们用一个小技巧,
+ // 就是直接使用 zap 本身的配置结构体来处理
+ cfg := zap.NewDevelopmentConfig()
+ err := viper.UnmarshalKey("log", &cfg)
+ if err != nil {
+ panic(err)
+ }
+ l, err := cfg.Build()
+ if err != nil {
+ panic(err)
+ }
+ return logger.NewZapLogger(l)
+}
diff --git a/webook/tag/ioc/redis.go b/webook/tag/ioc/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..bae1ec51b75c48e08ca02c54c5f88d0ef437aed2
--- /dev/null
+++ b/webook/tag/ioc/redis.go
@@ -0,0 +1,14 @@
+package ioc
+
+import (
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+)
+
+func InitRedis() redis.Cmdable {
+ addr := viper.GetString("redis.addr")
+ redisClient := redis.NewClient(&redis.Options{
+ Addr: addr,
+ })
+ return redisClient
+}
diff --git a/webook/tag/ioc/repository.go b/webook/tag/ioc/repository.go
new file mode 100644
index 0000000000000000000000000000000000000000..51f2289278531c02800556ea2adf883bc1a1fc68
--- /dev/null
+++ b/webook/tag/ioc/repository.go
@@ -0,0 +1,25 @@
+package ioc
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/tag/repository"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "time"
+)
+
+func InitRepository(d dao.TagDAO, c cache.TagCache, l logger.LoggerV1) repository.TagRepository {
+ repo := repository.NewTagRepository(d, c, l)
+ go func() {
+ // 执行缓存预加载
+ // 或者启动的环境变量
+ // 启动参数控制
+ // 或者借助配置中心的开关
+ ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+ defer cancel()
+ // 也可以同步执行。但是在一些场景下,同步执行会占用很长的时间,所以可以考虑异步执行。
+ repo.PreloadUserTags(ctx)
+ }()
+ return repo
+}
diff --git a/webook/tag/main.go b/webook/tag/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8e4db26cdbe45f76821c763b91ad47c759b4c57
--- /dev/null
+++ b/webook/tag/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func main() {
+ initViperV2Watch()
+ // 我要在初始化的过程中,把缓存加载好
+ // 谁来加载?
+ // dao, cache, repository, svc 谁来加载?
+ app := Init()
+ err := app.GRPCServer.Serve()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func initViperV2Watch() {
+ cfile := pflag.String("config",
+ "config/dev.yaml", "配置文件路径")
+ pflag.Parse()
+ // 直接指定文件路径
+ viper.SetConfigFile(*cfile)
+ viper.WatchConfig()
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/webook/tag/repository/cache/tag.go b/webook/tag/repository/cache/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..c9f6f2a97f77746592b65b2844ebb8c5ce19c012
--- /dev/null
+++ b/webook/tag/repository/cache/tag.go
@@ -0,0 +1,84 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "gitee.com/geekbang/basic-go/webook/tag/domain"
+ "github.com/redis/go-redis/v9"
+ "time"
+)
+
+var ErrKeyNotExist = redis.Nil
+
+type TagCache interface {
+ GetTags(ctx context.Context, uid int64) ([]domain.Tag, error)
+ Append(ctx context.Context, uid int64, tags ...domain.Tag) error
+ DelTags(ctx context.Context, uid int64) error
+}
+
+// Preload 全量加载?
+func Preload(ctx context.Context) {
+ // 你需要 gorm.DB
+}
+
+type RedisTagCache struct {
+ client redis.Cmdable
+ expiration time.Duration
+}
+
+func (r *RedisTagCache) DelTags(ctx context.Context, uid int64) error {
+ return r.client.Del(ctx, r.userTagsKey(uid)).Err()
+}
+
+func (r *RedisTagCache) Append(ctx context.Context, uid int64, tags ...domain.Tag) error {
+ data := make([]any, 0, len(tags))
+ key := r.userTagsKey(uid)
+ pip := r.client.Pipeline()
+ for _, tag := range tags {
+ val, err := json.Marshal(tag)
+ if err != nil {
+ return err
+ }
+ data = append(data, val)
+ //pip.HMSet(ctx, key, strconv.FormatInt(tag.Id, 10), val)
+ }
+
+ // 利用 pipeline 来执行,性能好一点
+
+ pip.RPush(ctx, key, data...)
+ // 你无法辨别 key 是不是已经有过期时间,
+ // 如果你是这个 key 的第一个 tag,你是没有过期时间
+ // 你可以不设置过期时间
+ pip.Expire(ctx, key, r.expiration)
+ _, err := pip.Exec(ctx)
+ return err
+}
+
+func (r *RedisTagCache) GetTags(ctx context.Context, uid int64) ([]domain.Tag, error) {
+ key := r.userTagsKey(uid)
+ data, err := r.client.LRange(ctx, key, 0, -1).Result()
+ if err != nil {
+ return nil, err
+ }
+ res := make([]domain.Tag, 0, len(data))
+ for _, ele := range data {
+ var t domain.Tag
+ err = json.Unmarshal([]byte(ele), &t)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, t)
+ }
+ return res, nil
+}
+
+func (r *RedisTagCache) userTagsKey(uid int64) string {
+ return fmt.Sprintf("tag:user_tags:%d", uid)
+}
+
+func NewRedisTagCache(client redis.Cmdable) TagCache {
+ return &RedisTagCache{
+ client: client,
+ }
+}
diff --git a/webook/tag/repository/dao/init.go b/webook/tag/repository/dao/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..c787eacc035ee89e06f07edae66be63a3501857c
--- /dev/null
+++ b/webook/tag/repository/dao/init.go
@@ -0,0 +1,10 @@
+package dao
+
+import "gorm.io/gorm"
+
+func InitTables(db *gorm.DB) error {
+ return db.AutoMigrate(
+ &Tag{},
+ &TagBiz{},
+ )
+}
diff --git a/webook/tag/repository/dao/tag.go b/webook/tag/repository/dao/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f326530ec6e3af9586aa50c7ae32b1a5889af5e
--- /dev/null
+++ b/webook/tag/repository/dao/tag.go
@@ -0,0 +1,137 @@
+package dao
+
+import (
+ "context"
+ "github.com/ecodeclub/ekit/slice"
+ "gorm.io/gorm"
+ "time"
+)
+
+// ID=1 => uid = 123
+// ID = 1 => uid =234
+type Tag struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ // 我要不要在这里创建一个唯一索引
+ Name string `gorm:"type=varchar(4096)"`
+ // 要在 uid 上创建一个索引
+ // 因为你有一个典型的根据 uid 来查询的场景
+ Uid int64 `gorm:"index"`
+ Ctime int64
+ Utime int64
+}
+
+// 某个人对某个资源打了标签
+type TagBiz struct {
+ Id int64 `gorm:"primaryKey,autoIncrement"`
+ BizId int64 `gorm:"index:biz_type_id"`
+ Biz string `gorm:"index:biz_type_id"`
+ // 冗余字段,加快查询和删除
+ // 这个字段可以删除的
+ Uid int64 `gorm:"index"`
+ //TagName string
+ Tid int64
+ Tag *Tag `gorm:"ForeignKey:Tid;AssociationForeignKey:Id;constraint:OnDelete:CASCADE"`
+ Ctime int64 `bson:"ctime,omitempty"`
+ Utime int64 `bson:"utime,omitempty"`
+}
+
+type TagDAO interface {
+ CreateTag(ctx context.Context, tag Tag) (int64, error)
+ CreateTagBiz(ctx context.Context, tagBiz []TagBiz) error
+ GetTagsByUid(ctx context.Context, uid int64) ([]Tag, error)
+ GetTagsByBiz(ctx context.Context, uid int64, biz string, bizId int64) ([]Tag, error)
+ GetTags(ctx context.Context, offset, limit int) ([]Tag, error)
+ GetTagsById(ctx context.Context, ids []int64) ([]Tag, error)
+}
+
+type GORMTagDAO struct {
+ db *gorm.DB
+}
+
+func (dao *GORMTagDAO) GetTagsById(ctx context.Context, ids []int64) ([]Tag, error) {
+ var res []Tag
+ err := dao.db.WithContext(ctx).Where("id IN ?", ids).Find(&res).Error
+ return res, err
+}
+
+func (dao *GORMTagDAO) CreateTag(ctx context.Context, tag Tag) (int64, error) {
+ now := time.Now().UnixMilli()
+ tag.Ctime = now
+ tag.Utime = now
+ err := dao.db.WithContext(ctx).Create(&tag).Error
+ return tag.Id, err
+}
+
+func (dao *GORMTagDAO) CreateTagBiz(ctx context.Context, tagBiz []TagBiz) error {
+ if len(tagBiz) == 0 {
+ return nil
+ }
+ now := time.Now().UnixMilli()
+ for _, t := range tagBiz {
+ t.Ctime = now
+ t.Utime = now
+ }
+ first := tagBiz[0]
+ return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ // 完成了覆盖式的操作
+ // 如果 tag_biz 里面没有 uid 字段。你的删除就很麻烦
+ // delete from tag_biz where tid IN
+ // (select distinct id from tag where uid = ?) AND biz = ? AND biz_id = ?
+ err := tx.Model(&TagBiz{}).Delete(
+ " uid = ? AND biz = ? AND biz_id = ?",
+ first.Uid, first.BizId, first.BizId).Error
+ if err != nil {
+ return err
+ }
+ return tx.Create(&tagBiz).Error
+ })
+}
+
+func (dao *GORMTagDAO) GetTagsByUid(ctx context.Context, uid int64) ([]Tag, error) {
+ var res []Tag
+ err := dao.db.WithContext(ctx).Where("uid= ?", uid).Find(&res).Error
+ return res, err
+}
+
+func (dao *GORMTagDAO) GetTagsByBiz(ctx context.Context, uid int64, biz string, bizId int64) ([]Tag, error) {
+ // 这边使用 JOIN 查询,如果你不想使用 JOIN 查询,
+ // 你就在 repository 里面分成两次查询
+ var res []TagBiz
+ err := dao.db.WithContext(ctx).Model(&TagBiz{}).
+ InnerJoins("Tag", dao.db.Model(&Tag{})).
+ Where("Tag.uid = ? AND biz = ? AND biz_id = ?", uid, biz, bizId).Find(&res).Error
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(res, func(idx int, src TagBiz) Tag {
+ return *src.Tag
+ }), nil
+
+ // 按照标准互联网的做法,是不用 JOIN 之类的查询,
+ // 分两次
+ //var tbs []TagBiz
+ //err := dao.db.WithContext(ctx).Where("uid =? AND biz = ? AND biz_id = ?").Find(&tbs).Error
+ //if err != nil {
+ // return nil, err
+ //}
+ //ids := slice.Map(tbs, func(idx int, src TagBiz) int64 {
+ // return src.Tid
+ //})
+ // 如果你有 id => tag 的缓存。或者 uid => tag 的缓存,你可以利用缓存
+ //var res []Tag
+ //err = dao.db.WithContext(ctx).Where("id IN ?", ids).Find(&res).Error
+
+ //return res, err
+}
+
+func (dao *GORMTagDAO) GetTags(ctx context.Context, offset, limit int) ([]Tag, error) {
+ var res []Tag
+ err := dao.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&res).Error
+ return res, err
+}
+
+func NewGORMTagDAO(db *gorm.DB) TagDAO {
+ return &GORMTagDAO{
+ db: db,
+ }
+}
diff --git a/webook/tag/repository/dao/tag_test.go b/webook/tag/repository/dao/tag_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c981afe8ca1c45c34cd0133d2cbbc0a86014e422
--- /dev/null
+++ b/webook/tag/repository/dao/tag_test.go
@@ -0,0 +1,25 @@
+package dao
+
+import (
+ "context"
+ "github.com/stretchr/testify/require"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "testing"
+)
+
+func TestGORMTagDAO_GetTagsByBiz(t *testing.T) {
+ // 这里你可以通过查看 SQL 来确定自己写的 JOIN 查询对不对
+ db, err := gorm.Open(sqlite.Open("gorm.db?mode=memory"), &gorm.Config{
+ // 只输出 SQL,不执行查询
+ DryRun: true,
+ })
+ require.NoError(t, err)
+ db = db.Debug()
+ dao := NewGORMTagDAO(db)
+ res, err := dao.GetTagsByBiz(context.Background(), 123, "test", 456)
+ if err != nil {
+ return
+ }
+ t.Log(res)
+}
diff --git a/webook/tag/repository/tag.go b/webook/tag/repository/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..4dd87566746f1305dd3b42b7db4fd6b3d4bc9688
--- /dev/null
+++ b/webook/tag/repository/tag.go
@@ -0,0 +1,174 @@
+package repository
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/tag/domain"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "github.com/ecodeclub/ekit/slice"
+ "time"
+)
+
+type TagRepository interface {
+ CreateTag(ctx context.Context, tag domain.Tag) (int64, error)
+ BindTagToBiz(ctx context.Context, uid int64, biz string, bizId int64, tags []int64) error
+ GetTags(ctx context.Context, uid int64) ([]domain.Tag, error)
+ GetTagsById(ctx context.Context, ids []int64) ([]domain.Tag, error)
+ GetBizTags(ctx context.Context, uid int64, biz string, bizId int64) ([]domain.Tag, error)
+}
+
+type CachedTagRepository struct {
+ dao dao.TagDAO
+ cache cache.TagCache
+ l logger.LoggerV1
+}
+
+// PreloadUserTags 在 toB 的场景下,你可以提前预加载缓存
+func (repo *CachedTagRepository) PreloadUserTags(ctx context.Context) error {
+ // 我怎么预加载?
+ // 缓存里面,究竟怎么存?
+ // 1. 放 json,json 里面是一个用户的所有的标签 uid => [{}, {}]
+ // 按照用户 ID 来查找
+ //var uid int64= 1
+ //for {
+ // repo.dao.GetTagsByUid(ctx, uid)
+ // uid ++
+ //}
+ // select * from tags group by uid
+ // 使用 redis 的数据结构
+ // 1. list
+ // 2. hash 用 hash 结构
+ // 3. set, sorted set 都可以
+
+ offset := 0
+ batch := 100
+ for {
+ dbCtx, cancel := context.WithTimeout(ctx, time.Second)
+ // 在这里还有一点点的优化手段,就是 GetTags 的时候,order by uid
+ tags, err := repo.dao.GetTags(dbCtx, offset, batch)
+ cancel()
+ if err != nil {
+ // 记录日志,然后返回
+ return err
+ }
+
+ // 按照 uid 进行分组,一个 uid 执行一次 append
+
+ // 这些 tag 是归属于不同的用户
+ for _, tag := range tags {
+ rctx, cancel := context.WithTimeout(ctx, time.Second)
+ err = repo.cache.Append(rctx, tag.Uid, repo.toDomain(tag))
+ cancel()
+ if err != nil {
+ // 记录日志,你可以中断,你也可以继续
+ continue
+ }
+ }
+ if len(tags) < batch {
+ return nil
+ }
+ offset += batch
+ }
+}
+
+func (repo *CachedTagRepository) GetTagsById(ctx context.Context, ids []int64) ([]domain.Tag, error) {
+ tags, err := repo.dao.GetTagsById(ctx, ids)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(tags, func(idx int, src dao.Tag) domain.Tag {
+ return repo.toDomain(src)
+ }), nil
+}
+
+func (repo *CachedTagRepository) BindTagToBiz(ctx context.Context, uid int64, biz string, bizId int64, tags []int64) error {
+ // 按照我们的说法,我们是要覆盖式地执行打标签
+ // 新的标签完全覆盖老的标签
+ // 按道理应该是 repository 去控制的
+ return repo.dao.CreateTagBiz(ctx, slice.Map(tags, func(idx int, src int64) dao.TagBiz {
+ return dao.TagBiz{
+ Tid: src,
+ BizId: bizId,
+ Biz: biz,
+ Uid: uid,
+ }
+ }))
+}
+
+func (repo *CachedTagRepository) GetTags(ctx context.Context, uid int64) ([]domain.Tag, error) {
+ res, err := repo.cache.GetTags(ctx, uid)
+ if err == nil {
+ return res, nil
+ }
+ // 下面也是慢路径,你同样可以说降级的时候不执行
+
+ // 如果我要缓存
+ // 我这里应该是 uid => tags 的映射
+ // toB 的时候,我直接全量缓存
+ // 我要在应用启动的时候,把缓存加载好
+ // 如果你认为你的 tags 是没有过期时间的,你这里就不用回查数据库了
+ tags, err := repo.dao.GetTagsByUid(ctx, uid)
+ if err != nil {
+ return nil, err
+ }
+
+ res = slice.Map(tags, func(idx int, src dao.Tag) domain.Tag {
+ return repo.toDomain(src)
+ })
+ err = repo.cache.Append(ctx, uid, res...)
+ if err != nil {
+ // 记录日志
+ // 缓存回写失败,不认为是一个问题
+ }
+ return res, nil
+}
+
+func (repo *CachedTagRepository) GetBizTags(ctx context.Context, uid int64, biz string, bizId int64) ([]domain.Tag, error) {
+ // 你要缓存的话,就是 uid + biz + biz_id 构成一个 key
+ tags, err := repo.dao.GetTagsByBiz(ctx, uid, biz, bizId)
+ if err != nil {
+ return nil, err
+ }
+ return slice.Map(tags, func(idx int, src dao.Tag) domain.Tag {
+ return repo.toDomain(src)
+ }), nil
+}
+
+func (repo *CachedTagRepository) CreateTag(ctx context.Context, tag domain.Tag) (int64, error) {
+ id, err := repo.dao.CreateTag(ctx, repo.toEntity(tag))
+ if err != nil {
+ return 0, err
+ }
+ // 也可以考虑用 DelTags
+ // 记得更新你的缓存
+ err = repo.cache.Append(ctx, tag.Uid, tag)
+ if err != nil {
+ // 记录日志
+ }
+ return id, nil
+}
+
+func NewTagRepository(tagDAO dao.TagDAO, c cache.TagCache, l logger.LoggerV1) *CachedTagRepository {
+ return &CachedTagRepository{
+ dao: tagDAO,
+ l: l,
+ cache: c,
+ }
+}
+
+func (repo *CachedTagRepository) toDomain(tag dao.Tag) domain.Tag {
+ return domain.Tag{
+ Id: tag.Id,
+ Name: tag.Name,
+ Uid: tag.Uid,
+ }
+}
+
+func (repo *CachedTagRepository) toEntity(tag domain.Tag) dao.Tag {
+ return dao.Tag{
+ Id: tag.Id,
+ Name: tag.Name,
+ Uid: tag.Uid,
+ }
+}
diff --git a/webook/tag/service/tag.go b/webook/tag/service/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..a0c15699dfc7565da8c99af2162306774f626e00
--- /dev/null
+++ b/webook/tag/service/tag.go
@@ -0,0 +1,77 @@
+package service
+
+import (
+ "context"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/tag/domain"
+ "gitee.com/geekbang/basic-go/webook/tag/events"
+ "gitee.com/geekbang/basic-go/webook/tag/repository"
+ "github.com/ecodeclub/ekit/slice"
+ "time"
+)
+
+type TagService interface {
+ CreateTag(ctx context.Context, uid int64, name string) (int64, error)
+ AttachTags(ctx context.Context, uid int64, biz string, bizId int64, tags []int64) error
+ GetTags(ctx context.Context, uid int64) ([]domain.Tag, error)
+ GetBizTags(ctx context.Context, uid int64, biz string, bizId int64) ([]domain.Tag, error)
+}
+
+type tagService struct {
+ repo repository.TagRepository
+ logger logger.LoggerV1
+ producer events.Producer
+}
+
+func (svc *tagService) AttachTags(ctx context.Context, uid int64, biz string, bizId int64, tags []int64) error {
+ err := svc.repo.BindTagToBiz(ctx, uid, biz, bizId, tags)
+ if err != nil {
+ return err
+ }
+ // 异步发送
+ go func() {
+ ts, err := svc.repo.GetTagsById(ctx, tags)
+ if err != nil {
+ // 记录日志
+ }
+ // 这里要根据 tag_index 的结构来定义
+ // 同样要注意顺序,即同一个用户对同一个资源打标签的顺序,
+ // 是不能乱的
+ pctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ err = svc.producer.ProduceSyncEvent(pctx, events.BizTags{
+ Uid: uid,
+ Biz: biz,
+ BizId: bizId,
+ Tags: slice.Map(ts, func(idx int, src domain.Tag) string {
+ return src.Name
+ }),
+ })
+ cancel()
+ if err != nil {
+ // 记录日志
+ }
+ }()
+ return err
+}
+
+func (svc *tagService) GetBizTags(ctx context.Context, uid int64, biz string, bizId int64) ([]domain.Tag, error) {
+ return svc.repo.GetBizTags(ctx, uid, biz, bizId)
+}
+
+func (svc *tagService) CreateTag(ctx context.Context, uid int64, name string) (int64, error) {
+ return svc.repo.CreateTag(ctx, domain.Tag{
+ Uid: uid,
+ Name: name,
+ })
+}
+
+func (svc *tagService) GetTags(ctx context.Context, uid int64) ([]domain.Tag, error) {
+ return svc.repo.GetTags(ctx, uid)
+}
+
+func NewTagService(repo repository.TagRepository, l logger.LoggerV1) TagService {
+ return &tagService{
+ repo: repo,
+ logger: l,
+ }
+}
diff --git a/webook/tag/wire.go b/webook/tag/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..55d32d680c2434a7b72edf52175a15da3fa20eaa
--- /dev/null
+++ b/webook/tag/wire.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "gitee.com/geekbang/basic-go/webook/pkg/wego"
+ "gitee.com/geekbang/basic-go/webook/tag/grpc"
+ "gitee.com/geekbang/basic-go/webook/tag/ioc"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/tag/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/tag/service"
+ "github.com/google/wire"
+)
+
+var thirdProvider = wire.NewSet(
+ ioc.InitRedis,
+ ioc.InitLogger,
+ ioc.InitDB,
+)
+
+func Init() *wego.App {
+ wire.Build(
+ thirdProvider,
+ cache.NewRedisTagCache,
+ dao.NewGORMTagDAO,
+ ioc.InitRepository,
+ service.NewTagService,
+ grpc.NewTagServiceServer,
+ ioc.InitGRPCxServer,
+ wire.Struct(new(wego.App), "GRPCServer"),
+ )
+ return new(wego.App)
+}
diff --git a/webook/wire.go b/webook/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..814a8d4c817df84f66861f4c0decde491299d6a8
--- /dev/null
+++ b/webook/wire.go
@@ -0,0 +1,96 @@
+//go:build wireinject
+
+package main
+
+import (
+ repository2 "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ cache2 "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ service2 "gitee.com/geekbang/basic-go/webook/interactive/service"
+ "gitee.com/geekbang/basic-go/webook/internal/events/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ article2 "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ article3 "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ ijwt "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/google/wire"
+)
+
+var interactiveSvcProvider = wire.NewSet(
+ service2.NewInteractiveService,
+ repository2.NewCachedInteractiveRepository,
+ dao2.NewGORMInteractiveDAO,
+ cache2.NewRedisInteractiveCache,
+)
+
+var rankingServiceSet = wire.NewSet(
+ repository.NewCachedRankingRepository,
+ cache.NewRankingRedisCache,
+ cache.NewRankingLocalCache,
+ service.NewBatchRankingService,
+)
+
+func InitWebServer() *App {
+ wire.Build(
+ // 最基础的第三方依赖
+ ioc.InitDB, ioc.InitRedis, ioc.InitRLockClient,
+ ioc.InitLogger,
+ ioc.InitKafka,
+ ioc.NewConsumers,
+ ioc.NewSyncProducer,
+
+ // 流量控制用的
+ //interactiveSvcProvider,
+ //ioc.InitIntrGRPCClient,
+
+ // 放一起,启用了 etcd 作为配置中心
+ ioc.InitEtcd,
+ ioc.InitIntrGRPCClientV1,
+ rankingServiceSet,
+ ioc.InitJobs,
+ ioc.InitRankingJob,
+
+ // consumer
+ article.NewKafkaProducer,
+
+ // 初始化 DAO
+ dao.NewUserDAO,
+ article3.NewGORMArticleDAO,
+
+ cache.NewUserCache,
+ cache.NewCodeCache,
+ cache.NewRedisArticleCache,
+
+ repository.NewUserRepository,
+ repository.NewCodeRepository,
+ article2.NewArticleRepository,
+
+ service.NewUserService,
+ service.NewCodeService,
+ service.NewArticleService,
+
+ // 直接基于内存实现
+ ioc.InitSMSService,
+ ioc.InitWechatService,
+
+ web.NewUserHandler,
+ web.NewArticleHandler,
+ web.NewOAuth2WechatHandler,
+ //ioc.NewWechatHandlerConfig,
+ ijwt.NewRedisJWTHandler,
+ // 你中间件呢?
+ // 你注册路由呢?
+ // 你这个地方没有用到前面的任何东西
+ //gin.Default,
+
+ ioc.InitWebServer,
+ ioc.InitMiddlewares,
+ // 组装我这个结构体的所有字段
+ wire.Struct(new(App), "*"),
+ )
+ return new(App)
+}
diff --git a/webook/wire_gen.go b/webook/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..c917802078886e88ce3eb7894f562f4d152666f0
--- /dev/null
+++ b/webook/wire_gen.go
@@ -0,0 +1,81 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package main
+
+import (
+ repository2 "gitee.com/geekbang/basic-go/webook/interactive/repository"
+ cache2 "gitee.com/geekbang/basic-go/webook/interactive/repository/cache"
+ dao2 "gitee.com/geekbang/basic-go/webook/interactive/repository/dao"
+ service2 "gitee.com/geekbang/basic-go/webook/interactive/service"
+ article3 "gitee.com/geekbang/basic-go/webook/internal/events/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository"
+ article2 "gitee.com/geekbang/basic-go/webook/internal/repository/article"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/cache"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao"
+ "gitee.com/geekbang/basic-go/webook/internal/repository/dao/article"
+ "gitee.com/geekbang/basic-go/webook/internal/service"
+ "gitee.com/geekbang/basic-go/webook/internal/web"
+ "gitee.com/geekbang/basic-go/webook/internal/web/jwt"
+ "gitee.com/geekbang/basic-go/webook/ioc"
+ "github.com/google/wire"
+)
+
+import (
+ _ "github.com/spf13/viper/remote"
+)
+
+// Injectors from wire.go:
+
+func InitWebServer() *App {
+ cmdable := ioc.InitRedis()
+ loggerV1 := ioc.InitLogger()
+ handler := jwt.NewRedisJWTHandler(cmdable)
+ v := ioc.InitMiddlewares(cmdable, loggerV1, handler)
+ db := ioc.InitDB(loggerV1)
+ userDAO := dao.NewUserDAO(db)
+ userCache := cache.NewUserCache(cmdable)
+ userRepository := repository.NewUserRepository(userDAO, userCache)
+ userService := service.NewUserService(userRepository, loggerV1)
+ codeCache := cache.NewCodeCache(cmdable)
+ codeRepository := repository.NewCodeRepository(codeCache)
+ smsService := ioc.InitSMSService(cmdable)
+ codeService := service.NewCodeService(codeRepository, smsService)
+ userHandler := web.NewUserHandler(userService, codeService, handler)
+ wechatService := ioc.InitWechatService(loggerV1)
+ oAuth2WechatHandler := web.NewOAuth2WechatHandler(wechatService, userService, handler)
+ articleDAO := article.NewGORMArticleDAO(db)
+ articleCache := cache.NewRedisArticleCache(cmdable)
+ articleRepository := article2.NewArticleRepository(articleDAO, articleCache, userRepository, loggerV1)
+ client := ioc.InitKafka()
+ syncProducer := ioc.NewSyncProducer(client)
+ producer := article3.NewKafkaProducer(syncProducer)
+ articleService := service.NewArticleService(articleRepository, loggerV1, producer)
+ clientv3Client := ioc.InitEtcd()
+ interactiveServiceClient := ioc.InitIntrGRPCClientV1(clientv3Client)
+ articleHandler := web.NewArticleHandler(articleService, interactiveServiceClient, loggerV1)
+ engine := ioc.InitWebServer(v, userHandler, oAuth2WechatHandler, articleHandler)
+ v2 := ioc.NewConsumers()
+ rankingRedisCache := cache.NewRankingRedisCache(cmdable)
+ rankingLocalCache := cache.NewRankingLocalCache()
+ rankingRepository := repository.NewCachedRankingRepository(rankingRedisCache, rankingLocalCache)
+ rankingService := service.NewBatchRankingService(articleService, rankingRepository, interactiveServiceClient)
+ rlockClient := ioc.InitRLockClient(cmdable)
+ rankingJob := ioc.InitRankingJob(rankingService, rlockClient, loggerV1)
+ cron := ioc.InitJobs(loggerV1, rankingJob)
+ app := &App{
+ web: engine,
+ consumers: v2,
+ cron: cron,
+ }
+ return app
+}
+
+// wire.go:
+
+var interactiveSvcProvider = wire.NewSet(service2.NewInteractiveService, repository2.NewCachedInteractiveRepository, dao2.NewGORMInteractiveDAO, cache2.NewRedisInteractiveCache)
+
+var rankingServiceSet = wire.NewSet(repository.NewCachedRankingRepository, cache.NewRankingRedisCache, cache.NewRankingLocalCache, service.NewBatchRankingService)
diff --git a/websocket/chat/client.go b/websocket/chat/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..9461c1ea06b6400caf28c4a57e6f198a6199ed5a
--- /dev/null
+++ b/websocket/chat/client.go
@@ -0,0 +1,137 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+const (
+ // Time allowed to write a message to the peer.
+ writeWait = 10 * time.Second
+
+ // Time allowed to read the next pong message from the peer.
+ pongWait = 60 * time.Second
+
+ // Send pings to peer with this period. Must be less than pongWait.
+ pingPeriod = (pongWait * 9) / 10
+
+ // Maximum message size allowed from peer.
+ maxMessageSize = 512
+)
+
+var (
+ newline = []byte{'\n'}
+ space = []byte{' '}
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+// Client is a middleman between the websocket connection and the hub.
+type Client struct {
+ hub *Hub
+
+ // The websocket connection.
+ conn *websocket.Conn
+
+ // Buffered channel of outbound messages.
+ send chan []byte
+}
+
+// readPump pumps messages from the websocket connection to the hub.
+//
+// The application runs readPump in a per-connection goroutine. The application
+// ensures that there is at most one reader on a connection by executing all
+// reads from this goroutine.
+func (c *Client) readPump() {
+ defer func() {
+ c.hub.unregister <- c
+ c.conn.Close()
+ }()
+ c.conn.SetReadLimit(maxMessageSize)
+ c.conn.SetReadDeadline(time.Now().Add(pongWait))
+ c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+ for {
+ _, message, err := c.conn.ReadMessage()
+ if err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
+ log.Printf("error: %v", err)
+ }
+ break
+ }
+ message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
+ c.hub.broadcast <- message
+ }
+}
+
+// writePump pumps messages from the hub to the websocket connection.
+//
+// A goroutine running writePump is started for each connection. The
+// application ensures that there is at most one writer to a connection by
+// executing all writes from this goroutine.
+func (c *Client) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer func() {
+ ticker.Stop()
+ c.conn.Close()
+ }()
+ for {
+ select {
+ case message, ok := <-c.send:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if !ok {
+ // The hub closed the channel.
+ c.conn.WriteMessage(websocket.CloseMessage, []byte{})
+ return
+ }
+
+ w, err := c.conn.NextWriter(websocket.TextMessage)
+ if err != nil {
+ return
+ }
+ w.Write(message)
+
+ // Add queued chat messages to the current websocket message.
+ n := len(c.send)
+ for i := 0; i < n; i++ {
+ w.Write(newline)
+ w.Write(<-c.send)
+ }
+
+ if err := w.Close(); err != nil {
+ return
+ }
+ case <-ticker.C:
+ c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ return
+ }
+ }
+ }
+}
+
+// serveWs handles websocket requests from the peer.
+func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
+ client.hub.register <- client
+
+ // Allow collection of memory referenced by the caller by doing all work in
+ // new goroutines.
+ go client.writePump()
+ go client.readPump()
+}
diff --git a/websocket/chat/home.html b/websocket/chat/home.html
new file mode 100644
index 0000000000000000000000000000000000000000..bf866affe39985b5761d1574cabadead50fe00bc
--- /dev/null
+++ b/websocket/chat/home.html
@@ -0,0 +1,98 @@
+
+
+
+Chat Example
+
+
+
+
+
+
+
+
diff --git a/websocket/chat/hub.go b/websocket/chat/hub.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb5c0e3bd34fbe06ab931f6c08ca78c946709b4c
--- /dev/null
+++ b/websocket/chat/hub.go
@@ -0,0 +1,53 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+// Hub maintains the set of active clients and broadcasts messages to the
+// clients.
+type Hub struct {
+ // Registered clients.
+ clients map[*Client]bool
+
+ // Inbound messages from the clients.
+ broadcast chan []byte
+
+ // Register requests from the clients.
+ register chan *Client
+
+ // Unregister requests from clients.
+ unregister chan *Client
+}
+
+func newHub() *Hub {
+ return &Hub{
+ broadcast: make(chan []byte),
+ register: make(chan *Client),
+ unregister: make(chan *Client),
+ clients: make(map[*Client]bool),
+ }
+}
+
+func (h *Hub) run() {
+ for {
+ select {
+ case client := <-h.register:
+ h.clients[client] = true
+ case client := <-h.unregister:
+ if _, ok := h.clients[client]; ok {
+ delete(h.clients, client)
+ close(client.send)
+ }
+ case message := <-h.broadcast:
+ for client := range h.clients {
+ select {
+ case client.send <- message:
+ default:
+ close(client.send)
+ delete(h.clients, client)
+ }
+ }
+ }
+ }
+}
diff --git a/websocket/chat/main.go b/websocket/chat/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..591cb896b199651461df521b8b22d8f2d46294b4
--- /dev/null
+++ b/websocket/chat/main.go
@@ -0,0 +1,45 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "flag"
+ "log"
+ "net/http"
+ "time"
+)
+
+var addr = flag.String("addr", ":8080", "http service address")
+
+func serveHome(w http.ResponseWriter, r *http.Request) {
+ log.Println(r.URL)
+ if r.URL.Path != "/" {
+ http.Error(w, "Not found", http.StatusNotFound)
+ return
+ }
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ http.ServeFile(w, r, "home.html")
+}
+
+func main() {
+ flag.Parse()
+ hub := newHub()
+ go hub.run()
+ http.HandleFunc("/", serveHome)
+ http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
+ serveWs(hub, w, r)
+ })
+ server := &http.Server{
+ Addr: *addr,
+ ReadHeaderTimeout: 3 * time.Second,
+ }
+ err := server.ListenAndServe()
+ if err != nil {
+ log.Fatal("ListenAndServe: ", err)
+ }
+}
diff --git a/websocket/forward_test.go b/websocket/forward_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..01fe9fa2d99da097226befcb85814b46137e169b
--- /dev/null
+++ b/websocket/forward_test.go
@@ -0,0 +1,73 @@
+package websocket
+
+import (
+ "github.com/ecodeclub/ekit/syncx"
+ "github.com/gorilla/websocket"
+ "log"
+ "net/http"
+ "testing"
+)
+
+// 集线器/中转站
+type Hub struct {
+ // syncx.Map 是我对 sync.Map 的一个简单封装
+ // 连上了我这个节点的所有的 websocket 的连接
+ // key 是客户端的名称
+ // 绝大多数情况下,你得存着这个东西
+ conns *syncx.Map[string, *websocket.Conn]
+
+ // sync.Map
+}
+
+func (h *Hub) AddConn(name string, conn *websocket.Conn) {
+ h.conns.Store(name, conn)
+ go func() {
+ // 准备接收数据
+ for {
+ // typ 是指 websocket 里面的消息类型
+ typ, msg, err := conn.ReadMessage()
+ // 这个 error 很难处理
+ if err != nil {
+ // 基本上这里都是代表连接出了问题
+ return
+ }
+ switch typ {
+ case websocket.CloseMessage:
+ h.conns.Delete(name)
+ conn.Close()
+ return
+ default:
+ // 要转发了
+ log.Println("来自客户端", name, typ, string(msg))
+ h.conns.Range(func(key string, value *websocket.Conn) bool {
+ if key == name {
+ // 自己的,就不需要转发了
+ return true
+ }
+ log.Println("转发给", key)
+ err := value.WriteMessage(typ, msg)
+ if err != nil {
+ log.Println(err)
+ }
+ return true
+ })
+ }
+ }
+ }()
+}
+
+func TestHub(t *testing.T) {
+ upgrader := &websocket.Upgrader{}
+ hub := &Hub{conns: &syncx.Map[string, *websocket.Conn]{}}
+ http.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
+ c, err := upgrader.Upgrade(writer, request, nil)
+ if err != nil {
+ // 升级失败
+ writer.Write([]byte("升级 ws 失败"))
+ return
+ }
+ name := request.URL.Query().Get("name")
+ hub.AddConn(name, c)
+ })
+ http.ListenAndServe(":8081", nil)
+}
diff --git a/websocket/server_test.go b/websocket/server_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..3a9e97a3aa55fa9219248df2ea154dd12616ea14
--- /dev/null
+++ b/websocket/server_test.go
@@ -0,0 +1,91 @@
+package websocket
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ "net/http"
+ "testing"
+ "time"
+)
+
+func TestServer(t *testing.T) {
+ upgrader := &websocket.Upgrader{}
+ http.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
+ // 这个就是用来搞升级的,或者说初始化 ws 的
+ // conn 代表一个 websocket 连接
+ c, err := upgrader.Upgrade(writer, request, nil)
+ if err != nil {
+ // 升级失败
+ writer.Write([]byte("升级 ws 失败"))
+ return
+ }
+ conn := &Ws{Conn: c}
+ // 从 websocket 接收数据
+ go func() {
+ for {
+ // typ 是指 websocket 里面的消息类型
+ typ, msg, err := conn.ReadMessage()
+ // 这个 error 很难处理
+ if err != nil {
+ // 基本上这里都是代表连接出了问题
+ return
+ }
+ switch typ {
+ case websocket.CloseMessage:
+ conn.Close()
+ return
+ default:
+ t.Log(typ, string(msg))
+ }
+ }
+ }()
+ go func() {
+ // 循环写一些消息到前端
+ ticker := time.NewTicker(time.Second * 3)
+ for now := range ticker.C {
+ err := conn.WriteString("hello, " + now.String())
+ if err != nil {
+ // 也是连接崩了
+ return
+ }
+ }
+ }()
+ })
+ go func() {
+ server := gin.Default()
+ server.GET("/", func(ctx *gin.Context) {
+ // req := ctx.Request
+ go func() {
+ // 在这里继续使用 ctx,就可能被坑
+
+ }()
+ //gorm.DB{}
+ //ctx.String()
+ })
+ //server.ServeHTTP()
+ server.Run(":8082")
+ }()
+ http.ListenAndServe(":8081", nil)
+}
+
+type Ws struct {
+ *websocket.Conn
+}
+
+func (ws *Ws) WriteString(data string) error {
+ err := ws.WriteMessage(websocket.TextMessage, []byte(data))
+ return err
+}
+
+//
+//func Read() {
+// var conn net.Conn
+//
+// for {
+// // 如果每一次都创建这个 buffer,
+// buffer := pool.Get()
+// conn.Read(buffer)
+// // 用完了
+// pool.Put(buffer)
+// }
+//}
diff --git a/websocket/simpleim/event.go b/websocket/simpleim/event.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1ce419150a350487083f4599b60cc37cf4dfbec
--- /dev/null
+++ b/websocket/simpleim/event.go
@@ -0,0 +1,19 @@
+package simpleim
+
+type Event struct {
+ Msg Message
+ // 接收者
+ Receiver int64
+ // 发送的 device
+ Device string
+}
+
+// EventV1 扩散只会和你有多少接入节点有关
+// 和群里面有多少人无关
+// 注册与发现机制,那么你就可以精确控制,转发到哪些节点
+type EventV1 struct {
+ Msg Message
+ Receivers []int64
+}
+
+const eventName = "simple_im_msg"
diff --git a/websocket/simpleim/gateway.go b/websocket/simpleim/gateway.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e2c149f63c3659b016b378b3cc82d6324ee7854
--- /dev/null
+++ b/websocket/simpleim/gateway.go
@@ -0,0 +1,231 @@
+package simpleim
+
+import (
+ "context"
+ "encoding/json"
+ "gitee.com/geekbang/basic-go/webook/pkg/logger"
+ "gitee.com/geekbang/basic-go/webook/pkg/saramax"
+ "github.com/IBM/sarama"
+ "github.com/ecodeclub/ekit/syncx"
+ "github.com/gorilla/websocket"
+ "log"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+type WsGateway struct {
+ // 连接了这个实例的客户端
+ // 这里我们用 uid 作为 key
+ // 实践中要考虑到不同的设备,
+ // 那么这个 key 可能是一个复合结构,例如 uid + 设备
+ conns *syncx.Map[int64, *Conn]
+ svc *IMService
+
+ client sarama.Client
+ instanceId string
+ upgrader *websocket.Upgrader
+}
+
+// Start 在这个启动的时候,监听 websocket 的请求,然后转发到后端
+func (g *WsGateway) Start(addr string) error {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/ws", g.wsHandler)
+ err := g.subscribeMsg()
+ if err != nil {
+ return err
+ }
+ return http.ListenAndServe(addr, mux)
+}
+
+// MessageV1 和前端约定好,具体的消息的内容的格式
+//type MessageV1 struct {
+//
+// // 这个是前端的序列号
+// // 不要求全局唯一的,正常只要当下这个 websocket 唯一就可以
+// Seq string
+//
+// // 谁发的?
+// // 能不能是前端传过来的?
+// // Sender int64
+//
+// // 发给谁
+// // cid channel id(group id),聊天 ID
+// // 单聊,也是用聊天 ID
+// Cid int64
+// // 内容
+// // Type 这个消息是什么消息
+// // 这个是你 IM 内部的类型
+// // type = "video", => content = url/资源标识符 key
+// // content 不可能是视频本身
+// // {"title": "GO从入门到入土", Addr: "https://oss.aliyun.com/im/resource/abc"}
+// @某人 {"metions": []int64, "text": }
+// Type string
+// // 你有文本消息,你有图片消息,你有视频消息
+// // 你这个 Content 究竟是什么?
+// Content string
+//
+// // 万一你每个消息都要校验 token,可以在这里带
+// //Token string
+//}
+
+func (g *WsGateway) subscribeMsg() error {
+ // 用 instance id 作为消费者组
+ // 不像业务里面,同样的节点同一个消费者组
+ // 每个节点单独的消费者组
+ cg, err := sarama.NewConsumerGroupFromClient(g.instanceId,
+ g.client)
+ if err != nil {
+ return err
+ }
+ go func() {
+ err := cg.Consume(context.Background(),
+ []string{eventName},
+ saramax.NewHandler[Event](logger.NewNoOpLogger(), g.consume))
+ if err != nil {
+ log.Println("退出监听消息循环", err)
+ }
+ }()
+ return nil
+}
+
+func (g *WsGateway) wsHandler(writer http.ResponseWriter, request *http.Request) {
+ conn, err := g.upgrader.Upgrade(writer, request, nil)
+ if err != nil {
+ // 升级失败
+ writer.Write([]byte("升级 ws 失败"))
+ return
+ }
+
+ // 在这里拿到 session。
+ // 如果我在这里拿到了 session
+ // 模拟我从 session/token 里面拿到 uid
+ c := &Conn{
+ Conn: conn,
+ }
+ uid := g.Uid(request)
+ // 我记录一下,哪些人连上了我
+ g.conns.Store(uid, c)
+ // 就是我得拿到你的 session
+ go func() {
+ defer func() {
+ g.conns.Delete(uid)
+ }()
+ for {
+ // 在这里监听用户发过来的消息
+ // typ 一般不需要处理,前端和你会约定好,typ 是什么
+ // websocket 这里你拿不到 token
+ typ, msgBytes, err := c.ReadMessage()
+ //switch err {
+ //case context.DeadlineExceeded:
+ // // 这个地方你是可以继续的
+ // continue
+ //case nil:
+ //
+ //default:
+ // // 都是网络出了问题,或者你的连接出了任务
+ // return
+ //}
+ if err != nil {
+ return
+ }
+
+ switch typ {
+ case websocket.TextMessage, websocket.BinaryMessage:
+ // 你是不是得知道,谁发的?发给谁?内容是什么?
+
+ var msg Message
+ err = json.Unmarshal(msgBytes, &msg)
+ if err != nil {
+ // 格式不对,正常不可能进来
+ continue
+ }
+
+ go func() {
+ // 我是建议开的
+ // 开 goroutine 的危险
+ // 搞协程池(任务池),控制住 goroutine 的数量
+ // 再开一个 goroutine
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ err = g.svc.Receive(ctx, uid, msg)
+ cancel()
+ if err != nil {
+ // 引入重试
+ // 你是不是要告诉前端,你出错了
+ // 前端怎么知道我哪条出错了?
+ err = c.Send(Message{
+ Seq: msg.Seq,
+ Type: "result",
+ Content: "failed",
+ })
+ if err != nil {
+ // 记录日志
+ // 这里也可以引入重试
+ }
+ }
+ }()
+
+ case websocket.CloseMessage:
+ c.Close()
+ default:
+
+ }
+ }
+ }()
+}
+
+// Uid 一般是从 jwt token 或者 session 里面取出来
+// 这里模拟从 header 里面读取出来
+func (g *WsGateway) Uid(req *http.Request) int64 {
+
+ // 拿到 token
+ //token := strings.TrimLeft(req.Header.Get("Authorization"), "Bearer ")
+ // jwt 解析
+ // jwt.Parse
+ // req.Cookie("sess_id")
+
+ uidStr := req.Header.Get("uid")
+ uid, _ := strconv.ParseInt(uidStr, 10, 64)
+ return uid
+}
+
+func (g *WsGateway) consume(msg *sarama.ConsumerMessage, evt Event) error {
+ // 转发
+ // 我怎么知道,这个 receiver 有没有连上我?
+ // 多端同步的时候,还需要知道哪个设备连上了我
+ receiverConn, ok := g.conns.Load(evt.Receiver)
+ if !ok {
+ return nil
+ }
+ return receiverConn.Send(evt.Msg)
+}
+
+// Conn 稍微做一个封装
+type Conn struct {
+ *websocket.Conn
+}
+
+func (c *Conn) Send(msg Message) error {
+ val, err := json.Marshal(msg)
+ if err != nil {
+ return err
+ }
+ return c.WriteMessage(websocket.TextMessage, val)
+}
+
+type Message struct {
+ // 发过来的消息的序列号
+ // 用于前后端关联消息
+ Seq string
+ // 这个是后端的 ID
+ // 前端有时候支持引用功能,转发功能的时候,会需要这个 ID
+ ID int64
+ // 用来标识不同的消息类型
+ // 文本消息,视频消息
+ // 系统消息(后端往前端发的,跟 IM 本身管理有关的消息)
+ Type string
+ Content string
+ // 聊天 ID,注意,正常来说这里不是记录目标用户 ID
+ // 而是记录代表了这个聊天的 ID
+ Cid int64
+}
diff --git a/websocket/simpleim/gateway_test.go b/websocket/simpleim/gateway_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6edebb8de1b2badab4411089c7b8dbef7297e64
--- /dev/null
+++ b/websocket/simpleim/gateway_test.go
@@ -0,0 +1,60 @@
+package simpleim
+
+import (
+ "github.com/IBM/sarama"
+ "github.com/ecodeclub/ekit/syncx"
+ "github.com/gorilla/websocket"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "testing"
+)
+
+type GatewayTestSuite struct {
+ suite.Suite
+ client sarama.Client
+}
+
+func (g *GatewayTestSuite) SetupSuite() {
+ cfg := sarama.NewConfig()
+ cfg.Producer.Return.Successes = true
+ cfg.Producer.Return.Errors = true
+ client, err := sarama.NewClient([]string{"localhost:9094"}, cfg)
+ g.client = client
+ require.NoError(g.T(), err)
+}
+
+func (g *GatewayTestSuite) TestGateway() {
+ // 启动三个实例,分别监听端口 8081,8082 和 8083,模拟分布式环境
+ go func() {
+ err := g.startGateway("gateway_8081", ":8081")
+ g.T().Log("8081 退出服务", err)
+ }()
+
+ go func() {
+ err := g.startGateway("gateway_8082", ":8082")
+ g.T().Log("8082 退出服务", err)
+ }()
+
+ err := g.startGateway("gateway_8083", ":8083")
+ g.T().Log("8083 退出服务", err)
+}
+
+func (g *GatewayTestSuite) startGateway(instance, addr string) error {
+ // 启动一个 gateway 的实例
+ producer, err := sarama.NewSyncProducerFromClient(g.client)
+ require.NoError(g.T(), err)
+ gateway := &WsGateway{
+ conns: &syncx.Map[int64, *Conn]{},
+ svc: &IMService{
+ producer: producer,
+ },
+ upgrader: &websocket.Upgrader{},
+ client: g.client,
+ instanceId: instance,
+ }
+ return gateway.Start(addr)
+}
+
+func TestWsGateway(t *testing.T) {
+ suite.Run(t, new(GatewayTestSuite))
+}
diff --git a/websocket/simpleim/service.go b/websocket/simpleim/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..e1ff7e86214b2d745a287d7002d12ae1e3f31cb4
--- /dev/null
+++ b/websocket/simpleim/service.go
@@ -0,0 +1,56 @@
+package simpleim
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/IBM/sarama"
+ "strconv"
+)
+
+// IMService 代表了我们后端服务
+type IMService struct {
+ producer sarama.SyncProducer
+}
+
+func (s *IMService) Receive(ctx context.Context, sender int64, msg Message) error {
+ // 这边就是业务的大头
+
+ // 审核,如果不通过,就拒绝,也在这个地方,一定是同步的,而且最先执行
+
+ // 我要先找到接收者..
+ receivers := s.findMembers()
+
+ // 同步数据过去给搜索,你可以在这里做,也可以借助消费 eventName 来
+ // 消息记录存储,也可以在这里做。一般存一条
+
+ for _, receiver := range receivers {
+ if receiver == sender {
+ // 你自己就不要转发了
+ // 但是,如果你有多端同步,你还得转发
+ continue
+ }
+ // 一个个转发
+ // 你要注意一点
+ // 正常来说,这边你可以考虑顺序问题了
+ // 这边。你可以考虑改批量接口
+ event, _ := json.Marshal(Event{Msg: msg, Receiver: receiver})
+ _, _, err := s.producer.SendMessage(&sarama.ProducerMessage{
+ Topic: eventName,
+ // 可以考虑,在初始话 producer 的时候,使用哈希类的 partition 选取策略
+ Key: sarama.StringEncoder(strconv.FormatInt(receiver, 10)),
+ Value: sarama.ByteEncoder(event),
+ })
+ if err != nil {
+ // 记录日志 + 重试
+ continue
+ }
+ }
+ return nil
+}
+
+// 这里模拟根据 cid,也就是聊天 ID 来查找参与了该聊天的成员
+func (s *IMService) findMembers() []int64 {
+ // 固定返回 1,2,3,4
+ // 正常来说,你这里要去找你的聊天的成员
+ return []int64{1, 2, 3, 4}
+}
diff --git a/wire/db.go b/wire/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..20e405a14b65c19371cf4091c2633b377928dc6b
--- /dev/null
+++ b/wire/db.go
@@ -0,0 +1,14 @@
+package wire
+
+import (
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func InitDB() *gorm.DB {
+ db, err := gorm.Open(mysql.Open("dsn"))
+ if err != nil {
+ panic(err)
+ }
+ return db
+}
diff --git a/wire/main.go b/wire/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd7a8a8b5c35e9956caa298706f2c9e8a955ea8b
--- /dev/null
+++ b/wire/main.go
@@ -0,0 +1,21 @@
+package wire
+
+import (
+ "fmt"
+ "gitee.com/geekbang/basic-go/wire/repository"
+ "gitee.com/geekbang/basic-go/wire/repository/dao"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func main() {
+ db, err := gorm.Open(mysql.Open("dsn"))
+ if err != nil {
+ panic(err)
+ }
+ ud := dao.NewUserDAO(db)
+ repo := repository.NewUserRepository(ud)
+ fmt.Println(repo)
+
+ InitRepository()
+}
diff --git a/wire/repository/dao/user.go b/wire/repository/dao/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..8e57f17a504f1eb50ba3cad6cf59ac94a384a796
--- /dev/null
+++ b/wire/repository/dao/user.go
@@ -0,0 +1,13 @@
+package dao
+
+import "gorm.io/gorm"
+
+type UserDAO struct {
+ db *gorm.DB
+}
+
+func NewUserDAO(db *gorm.DB) *UserDAO {
+ return &UserDAO{
+ db: db,
+ }
+}
diff --git a/wire/repository/user.go b/wire/repository/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..aac8a5e7a550649c83524ebd3b5752d37aed7e31
--- /dev/null
+++ b/wire/repository/user.go
@@ -0,0 +1,13 @@
+package repository
+
+import "gitee.com/geekbang/basic-go/wire/repository/dao"
+
+type UserRepository struct {
+ dao *dao.UserDAO
+}
+
+func NewUserRepository(dao *dao.UserDAO) *UserRepository {
+ return &UserRepository{
+ dao: dao,
+ }
+}
diff --git a/wire/wire.go b/wire/wire.go
new file mode 100644
index 0000000000000000000000000000000000000000..3ce0f93fdc15d4b7495de715c8c2b4ee2e022952
--- /dev/null
+++ b/wire/wire.go
@@ -0,0 +1,18 @@
+//go:build wireinject
+
+// 让 wire 来注入这里的代码
+package wire
+
+import (
+ "gitee.com/geekbang/basic-go/wire/repository"
+ "gitee.com/geekbang/basic-go/wire/repository/dao"
+ "github.com/google/wire"
+)
+
+func InitRepository() *repository.UserRepository {
+ // 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序
+ // 这个方法里面传入各个组件的初始化方法
+ wire.Build(InitDB, repository.NewUserRepository,
+ dao.NewUserDAO)
+ return new(repository.UserRepository)
+}
diff --git a/wire/wire_gen.go b/wire/wire_gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..f07678fe524f2e49675ff80d6cdd7f488bff30b5
--- /dev/null
+++ b/wire/wire_gen.go
@@ -0,0 +1,21 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package wire
+
+import (
+ "gitee.com/geekbang/basic-go/wire/repository"
+ "gitee.com/geekbang/basic-go/wire/repository/dao"
+)
+
+// Injectors from wire.go:
+
+func InitRepository() *repository.UserRepository {
+ db := InitDB()
+ userDAO := dao.NewUserDAO(db)
+ userRepository := repository.NewUserRepository(userDAO)
+ return userRepository
+}