feat: 初始化shop-crm-agent项目,实现基础CLI与CRM操作能力
本提交初始化完整的shop-crm-agent项目: 1. 创建基础项目结构与配置文件(.gitignore、go.mod/go.sum) 2. 添加项目许可协议与文档(README.md、AGENTS.md、SKILL.md) 3. 实现核心CLI入口与命令框架 4. 封装Protobuf API客户端与完整的命令实现 5. 添加单元测试覆盖核心功能与边界情况 6. 提供构建脚本与版本管理能力
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/bin/
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.tmp
|
||||
@@ -0,0 +1,37 @@
|
||||
# AGENTS.md
|
||||
|
||||
本仓库是 `shop-crm-agent` 的唯一源码仓库。默认使用中文沟通。
|
||||
|
||||
## 项目边界
|
||||
|
||||
- 客户端只通过 `/api/shop/crm-agent/v1` Protobuf HTTP API 工作,不直接访问数据库、Redis、LightCore 配置或后端运行容器。
|
||||
- Go module 固定为 `code.zhecent.com/open/shop-crm-agent`,不得引入 LightCore backend module、Gin、GORM、数据库驱动或 Redis 客户端。
|
||||
- `internal/api/shop_crm_agent.pb.go` 是生成代码,不得手工编辑。
|
||||
- Protobuf schema 的唯一真相位于同级 `../LightCore/backend/application/shop/interfaces/crm_agent_proto/proto/shop_crm_agent.proto`。
|
||||
- 协议变更必须从 `../LightCore/backend` 执行 `bash scripts/proto_gen_shop_crm_agent.sh`,同时检查两个仓库的生成结果和测试。
|
||||
|
||||
## CLI 契约
|
||||
|
||||
- 所有业务命令输出 Protobuf JSON,不输出表格或混合说明文本。
|
||||
- 业务失败先输出服务端结构化响应再返回非零退出码;运输或解析失败不得伪造业务响应。
|
||||
- 写命令必须使用稳定且唯一的 `--request-id`;同一操作重试时复用原 request ID 和原参数。
|
||||
- 报价编辑固定使用 `begin -> command -> preview -> commit`,放弃时使用 `discard`。
|
||||
- 项目和报价删除必须先执行对应 `delete-preview`,不得绕过 revision 与 `changeSetId`。
|
||||
- CLI 只读取 `LIGHTCORE_API_BASE_URL` 和 `LIGHTCORE_SHOP_CRM_AGENT_TOKEN`;缺失时明确失败,不回退 localhost。
|
||||
- Token 不得写入源码、文件、日志或输出;测试只能使用明显虚构的占位值。
|
||||
|
||||
## 开发流程
|
||||
|
||||
1. 先读取相邻实现和 `SKILL.md`,确认命令边界与安全流程。
|
||||
2. 只修改职责明确的 command/client 代码,不在客户端复制服务端业务规则。
|
||||
3. 生成代码只通过 LightCore 的协议生成脚本更新。
|
||||
4. 执行 `go test ./...` 和 `go build ./...`。
|
||||
5. 执行 `go run ./scripts/build.go`,确认 `bin/shop-crm-agent[.exe] --version` 可运行。
|
||||
6. 用 `rg` 检查 Token、Secret、内部绝对路径和意外的后端依赖。
|
||||
|
||||
## Git 与更新
|
||||
|
||||
- `main` 必须始终可测试、可构建;禁止 force push。
|
||||
- 更新只使用 `git pull --ff-only`。
|
||||
- 工作区有本地修改时不得自动覆盖、重置或清理。
|
||||
- 本仓库不提交编译产物,不提供二进制自更新或预编译 Release。
|
||||
@@ -0,0 +1,12 @@
|
||||
Copyright (c) 2026 Zhecent. All rights reserved.
|
||||
|
||||
This source code is made publicly readable for review and for authorized
|
||||
Zhecent employees to use solely for Zhecent's internal business purposes.
|
||||
|
||||
No permission is granted to any other person or organization to copy, modify,
|
||||
distribute, sublicense, sell, or otherwise use this source code without prior
|
||||
written permission from Zhecent.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
||||
@@ -0,0 +1,75 @@
|
||||
# shop-crm-agent
|
||||
|
||||
`shop-crm-agent` 是通过正式 Protobuf HTTP API 操作 Shop CRM 项目和独立报价的受控命令行客户端。源码允许匿名读取,但使用业务 API 仍需要管理员签发的 CRM Agent 凭据。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Git
|
||||
- Go 1.26 或更高版本
|
||||
- 可访问目标 LightCore API 的网络环境
|
||||
- 运行环境提供 `LIGHTCORE_API_BASE_URL` 和 `LIGHTCORE_SHOP_CRM_AGENT_TOKEN`
|
||||
|
||||
仓库在开发工作区中固定与 `LightCore` 同级。例如:
|
||||
|
||||
```text
|
||||
<workspace>/
|
||||
├── LightCore/
|
||||
└── shop-crm-agent/
|
||||
```
|
||||
|
||||
普通构建不依赖 `LightCore` 仓库;只有修改 Protobuf 契约时才需要同级的 LightCore 源码。
|
||||
|
||||
## 首次安装
|
||||
|
||||
```bash
|
||||
git clone https://code.zhecent.com/open/shop-crm-agent.git
|
||||
cd shop-crm-agent
|
||||
go run ./scripts/build.go
|
||||
./bin/shop-crm-agent --version
|
||||
```
|
||||
|
||||
Windows 构建产物为 `bin/shop-crm-agent.exe`。构建脚本执行测试后,只编译当前操作系统和 CPU 架构,不安装系统服务,也不修改全局 PATH。
|
||||
|
||||
运行业务命令前,由当前 Shell 或 Agent 的受控运行环境提供配置:
|
||||
|
||||
```bash
|
||||
export LIGHTCORE_API_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
`LIGHTCORE_SHOP_CRM_AGENT_TOKEN` 必须通过密码管理器、受控 Secret 注入或其他不会回显和记录明文的机制提供。不要在交互式命令中直接输入真实 Token,也不得把它写入仓库、Skill、`.env`、命令历史、普通日志或聊天内容。
|
||||
|
||||
## 更新
|
||||
|
||||
收到内部更新通知后,让 Agent 在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git pull --ff-only
|
||||
```
|
||||
|
||||
确认拉取成功后,Agent 必须重新读取最新的 `SKILL.md`,再执行:
|
||||
|
||||
```bash
|
||||
go run ./scripts/build.go
|
||||
./bin/shop-crm-agent --version
|
||||
```
|
||||
|
||||
如果工作区存在本地修改,必须先停止更新并确认修改来源,不得强制重置。只有重新构建和版本探针都通过后才可继续执行 CRM 操作。
|
||||
|
||||
## 开发验证
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
go build ./...
|
||||
go run ./scripts/build.go
|
||||
```
|
||||
|
||||
`internal/api/shop_crm_agent.pb.go` 是生成代码,不得手工编辑。协议源文件位于同级 LightCore 仓库,生成入口为:
|
||||
|
||||
```bash
|
||||
(cd ../LightCore/backend && bash scripts/proto_gen_shop_crm_agent.sh)
|
||||
```
|
||||
|
||||
## 许可
|
||||
|
||||
本仓库保留全部权利,仅授权 Zhecent 员工用于公司内部业务。详见 `LICENSE`。
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: lc-shop-crm-agent
|
||||
description: 安装、更新并安全使用 shop-crm-agent 查询和维护 Shop CRM 项目与独立报价。
|
||||
---
|
||||
|
||||
# lc-shop-crm-agent - Shop CRM Agent 操作流程
|
||||
|
||||
## 目的
|
||||
|
||||
指导 AI Agent 从公开源码构建 `shop-crm-agent`,并通过正式 Protobuf API 安全操作 CRM 项目和独立报价。
|
||||
|
||||
## 安装与更新
|
||||
|
||||
1. 仓库固定 clone 为工作区中的 `shop-crm-agent` 目录;与 LightCore 联合开发时二者必须同级。
|
||||
2. 缺少仓库时,从 `https://code.zhecent.com/open/shop-crm-agent.git` 匿名 clone,不使用来源不明的压缩包或二进制。
|
||||
3. 检查 `go version` 满足 `go.mod`;缺少 Go 时安装官方 Go 工具链,再继续构建。
|
||||
4. 首次安装在仓库根目录执行 `go run ./scripts/build.go`,使用 `bin/shop-crm-agent`;Windows 使用 `bin/shop-crm-agent.exe`。
|
||||
5. 收到更新通知后先确认 `git status --short` 为空,再执行 `git pull --ff-only`;拉取后重新读取本文件并重新构建。
|
||||
6. 本地存在修改、拉取失败、测试失败、构建失败或版本探针失败时停止,不得强制重置或继续使用半完成产物。
|
||||
|
||||
## 运行配置
|
||||
|
||||
- 确认 `LIGHTCORE_API_BASE_URL` 和 `LIGHTCORE_SHOP_CRM_AGENT_TOKEN` 已由运行环境提供;不得显示、记录或提交 Token。
|
||||
- 凭据只通过 Shop 管理后台创建和撤销;CLI 不管理凭据。
|
||||
- 业务命令使用仓库 `bin/` 下的当前构建产物,精确参数以 `<binary> <subcommand> --help` 为准。
|
||||
|
||||
## 操作流程
|
||||
|
||||
1. 创建项目前用 `customer-list` 获取真实客户 ID;其他操作先查询真实项目、报价、SKU、层级和产品行 ID。
|
||||
2. 每个写命令使用稳定且唯一的 `--request-id`;重试同一操作时复用原 ID 和完全相同的参数。
|
||||
3. 报价内容修改严格执行 `begin -> command -> preview -> commit`;不提交时执行 `discard`。
|
||||
4. 删除先执行 `delete-preview`,只使用其返回的 revision 和 `changeSetId`;`allowed=false` 时停止。
|
||||
5. 读取 JSON 响应的 `header.code`、`header.replayed`、revision、`changeSetId` 与 `affectedLineIds`,不得只凭进程退出码推断业务状态。
|
||||
|
||||
## 阻塞检查点
|
||||
|
||||
- 需求超出项目主线和独立报价时停止;不得转向客户维护、合同、设计画板、跟进、资金、文件、采购或产品库维护。
|
||||
- 产品库行只提交真实 `sku-id`,不得尝试修改产品库成本;自定义行仅通过明确 flag 修改自身成本。
|
||||
- 非空层级删除只有用户意图明确时才使用 `--cascade`。
|
||||
- 项目非空、报价锁定、revision 变化、changeSet 不一致或回执状态未知时不得强行继续。
|
||||
- 请求取消、超时或结果不确定时先查询资源状态,不得盲目生成新 request ID 重复写入。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
go build ./...
|
||||
go run ./scripts/build.go
|
||||
./bin/shop-crm-agent --version
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.zhecent.com/open/shop-crm-agent/internal/command"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
if err := command.New(version).Execute(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module code.zhecent.com/open/shop-crm-agent
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "code.zhecent.com/open/shop-crm-agent/internal/api"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
apiPath = "/api/shop/crm-agent/v1"
|
||||
maxResponseSize = 16 << 20
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, token string, httpClient *http.Client) (*Client, error) {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
parsed, err := url.Parse(baseURL)
|
||||
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return nil, errors.New("LIGHTCORE_API_BASE_URL 必须是有效的 http/https URL")
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return nil, errors.New("LIGHTCORE_SHOP_CRM_AGENT_TOKEN 不能为空")
|
||||
}
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
return &Client{baseURL: baseURL, token: strings.TrimSpace(token), httpClient: httpClient}, nil
|
||||
}
|
||||
|
||||
type responseWithHeader interface {
|
||||
proto.Message
|
||||
GetHeader() *api.ResponseHeader
|
||||
}
|
||||
|
||||
func doRequest[Req proto.Message, Resp responseWithHeader](ctx context.Context, client *Client, path string, request Req, response Resp) (Resp, error) {
|
||||
data, err := proto.Marshal(request)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("编码 Protobuf 请求失败: %w", err)
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+apiPath+path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/x-protobuf")
|
||||
httpRequest.Header.Set("Accept", "application/x-protobuf")
|
||||
httpRequest.Header.Set("Authorization", "Bearer "+client.token)
|
||||
httpResponse, err := client.httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(httpResponse.Body, maxResponseSize+1))
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
if len(body) > maxResponseSize {
|
||||
return response, errors.New("CRM Agent API 响应超过16 MiB限制")
|
||||
}
|
||||
if err := proto.Unmarshal(body, response); err != nil {
|
||||
return response, fmt.Errorf("解析 Protobuf 响应失败(HTTP %d): %w", httpResponse.StatusCode, err)
|
||||
}
|
||||
header := response.GetHeader()
|
||||
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
|
||||
if header != nil && header.Message != "" {
|
||||
return response, fmt.Errorf("CRM Agent API HTTP %d: %s", httpResponse.StatusCode, header.Message)
|
||||
}
|
||||
return response, fmt.Errorf("CRM Agent API HTTP %d", httpResponse.StatusCode)
|
||||
}
|
||||
if header == nil {
|
||||
return response, errors.New("CRM Agent API 响应缺少 ResponseHeader")
|
||||
}
|
||||
if header.Code != 200 {
|
||||
return response, fmt.Errorf("CRM Agent API 错误 %d: %s", header.Code, header.Message)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) CustomerList(ctx context.Context, req *api.CustomerListRequest) (*api.CustomerListResponse, error) {
|
||||
return doRequest(ctx, c, "/customers/list", req, &api.CustomerListResponse{})
|
||||
}
|
||||
func (c *Client) ProjectList(ctx context.Context, req *api.ProjectListRequest) (*api.ProjectListResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/list", req, &api.ProjectListResponse{})
|
||||
}
|
||||
func (c *Client) ProjectGet(ctx context.Context, req *api.ProjectGetRequest) (*api.ProjectResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/get", req, &api.ProjectResponse{})
|
||||
}
|
||||
func (c *Client) ProjectCreate(ctx context.Context, req *api.ProjectCreateRequest) (*api.ProjectResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/create", req, &api.ProjectResponse{})
|
||||
}
|
||||
func (c *Client) ProjectUpdate(ctx context.Context, req *api.ProjectUpdateRequest) (*api.ProjectResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/update", req, &api.ProjectResponse{})
|
||||
}
|
||||
func (c *Client) ProjectStatus(ctx context.Context, req *api.ProjectStatusRequest) (*api.ProjectResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/status", req, &api.ProjectResponse{})
|
||||
}
|
||||
func (c *Client) ProjectDeletePreview(ctx context.Context, req *api.ProjectDeletePreviewRequest) (*api.DeletePreviewResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/delete/preview", req, &api.DeletePreviewResponse{})
|
||||
}
|
||||
func (c *Client) ProjectDelete(ctx context.Context, req *api.ProjectDeleteRequest) (*api.DeletePreviewResponse, error) {
|
||||
return doRequest(ctx, c, "/projects/delete", req, &api.DeletePreviewResponse{})
|
||||
}
|
||||
|
||||
func (c *Client) QuotationList(ctx context.Context, req *api.QuotationListRequest) (*api.QuotationListResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/list", req, &api.QuotationListResponse{})
|
||||
}
|
||||
func (c *Client) QuotationGet(ctx context.Context, req *api.QuotationGetRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/get", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationCreate(ctx context.Context, req *api.QuotationCreateRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/create", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationCopy(ctx context.Context, req *api.QuotationCopyRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/copy", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationTitle(ctx context.Context, req *api.QuotationTitleRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/title", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationStatus(ctx context.Context, req *api.QuotationStatusRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/status", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationLock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/lock", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationUnlock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/unlock", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) QuotationDeletePreview(ctx context.Context, req *api.QuotationDeletePreviewRequest) (*api.DeletePreviewResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/delete/preview", req, &api.DeletePreviewResponse{})
|
||||
}
|
||||
func (c *Client) QuotationDelete(ctx context.Context, req *api.QuotationDeleteRequest) (*api.DeletePreviewResponse, error) {
|
||||
return doRequest(ctx, c, "/quotations/delete", req, &api.DeletePreviewResponse{})
|
||||
}
|
||||
func (c *Client) CatalogSearch(ctx context.Context, req *api.CatalogSearchRequest) (*api.CatalogSearchResponse, error) {
|
||||
return doRequest(ctx, c, "/catalog/search", req, &api.CatalogSearchResponse{})
|
||||
}
|
||||
|
||||
func (c *Client) EditBegin(ctx context.Context, req *api.QuotationEditBeginRequest) (*api.EditSessionResponse, error) {
|
||||
return doRequest(ctx, c, "/quotation/edit/begin", req, &api.EditSessionResponse{})
|
||||
}
|
||||
func (c *Client) EditCommand(ctx context.Context, req *api.QuotationEditCommandRequest) (*api.QuotationEditCommandResponse, error) {
|
||||
return doRequest(ctx, c, "/quotation/edit/command", req, &api.QuotationEditCommandResponse{})
|
||||
}
|
||||
func (c *Client) EditPreview(ctx context.Context, req *api.QuotationEditPreviewRequest) (*api.QuotationEditPreviewResponse, error) {
|
||||
return doRequest(ctx, c, "/quotation/edit/preview", req, &api.QuotationEditPreviewResponse{})
|
||||
}
|
||||
func (c *Client) EditCommit(ctx context.Context, req *api.QuotationEditCommitRequest) (*api.QuotationResponse, error) {
|
||||
return doRequest(ctx, c, "/quotation/edit/commit", req, &api.QuotationResponse{})
|
||||
}
|
||||
func (c *Client) EditDiscard(ctx context.Context, req *api.QuotationEditDiscardRequest) (*api.EditSessionResponse, error) {
|
||||
return doRequest(ctx, c, "/quotation/edit/discard", req, &api.EditSessionResponse{})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
api "code.zhecent.com/open/shop-crm-agent/internal/api"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestClientSendsBearerProtobufAndChecksBusinessHeader(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path != apiPath+"/projects/get" {
|
||||
t.Errorf("path = %s", request.URL.Path)
|
||||
}
|
||||
if request.Header.Get("Authorization") != "Bearer test-token" {
|
||||
t.Errorf("authorization = %q", request.Header.Get("Authorization"))
|
||||
}
|
||||
if request.Header.Get("Content-Type") != "application/x-protobuf" {
|
||||
t.Errorf("content type = %q", request.Header.Get("Content-Type"))
|
||||
}
|
||||
data, _ := proto.Marshal(&api.ProjectResponse{Header: &api.ResponseHeader{Code: 200, Message: "success"}, Project: &api.Project{Id: 9}})
|
||||
writer.Header().Set("Content-Type", "application/x-protobuf")
|
||||
_, _ = writer.Write(data)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := New(server.URL, "test-token", server.Client())
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{ProjectId: 9})
|
||||
if err != nil {
|
||||
t.Fatalf("project get: %v", err)
|
||||
}
|
||||
if response.Project.GetId() != 9 {
|
||||
t.Fatalf("project id = %d", response.Project.GetId())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReportsHTTPProtocolAndSizeFailures(t *testing.T) {
|
||||
t.Run("http error preserves protobuf header", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
data, _ := proto.Marshal(&api.ProjectResponse{Header: &api.ResponseHeader{Code: 401, Message: "unauthorized"}})
|
||||
_, _ = writer.Write(data)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, _ := New(server.URL, "token", server.Client())
|
||||
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
|
||||
if err == nil || !strings.Contains(err.Error(), "HTTP 401: unauthorized") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
if response.GetHeader().GetCode() != 401 {
|
||||
t.Fatalf("header = %#v", response.GetHeader())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed protobuf", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write([]byte{0xff})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, _ := New(server.URL, "token", server.Client())
|
||||
if _, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{}); err == nil || !strings.Contains(err.Error(), "解析 Protobuf 响应失败") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("response size limit", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write(make([]byte, maxResponseSize+1))
|
||||
}))
|
||||
defer server.Close()
|
||||
client, _ := New(server.URL, "token", server.Client())
|
||||
if _, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{}); err == nil || !strings.Contains(err.Error(), "16 MiB") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClientHonorsHTTPClientTimeout(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}))
|
||||
defer server.Close()
|
||||
httpClient := server.Client()
|
||||
httpClient.Timeout = time.Millisecond
|
||||
client, _ := New(server.URL, "token", httpClient)
|
||||
_, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
|
||||
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRepresentativeOperationRoutes(t *testing.T) {
|
||||
var paths []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
paths = append(paths, request.URL.Path)
|
||||
var response proto.Message
|
||||
switch request.URL.Path {
|
||||
case apiPath + "/projects/create":
|
||||
response = &api.ProjectResponse{Header: &api.ResponseHeader{Code: 200}}
|
||||
case apiPath + "/quotations/delete/preview":
|
||||
response = &api.DeletePreviewResponse{Header: &api.ResponseHeader{Code: 200}}
|
||||
case apiPath + "/quotation/edit/command":
|
||||
response = &api.QuotationEditCommandResponse{Header: &api.ResponseHeader{Code: 200}}
|
||||
default:
|
||||
t.Fatalf("unexpected path %q", request.URL.Path)
|
||||
}
|
||||
data, _ := proto.Marshal(response)
|
||||
_, _ = writer.Write(data)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, _ := New(server.URL, "token", server.Client())
|
||||
if _, err := client.ProjectCreate(context.Background(), &api.ProjectCreateRequest{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.QuotationDeletePreview(context.Background(), &api.QuotationDeletePreviewRequest{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.EditCommand(context.Background(), &api.QuotationEditCommandRequest{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{apiPath + "/projects/create", apiPath + "/quotations/delete/preview", apiPath + "/quotation/edit/command"}
|
||||
if strings.Join(paths, "|") != strings.Join(want, "|") {
|
||||
t.Fatalf("paths = %v", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRejectsMissingConfigurationAndBusinessError(t *testing.T) {
|
||||
if _, err := New("", "token", nil); err == nil {
|
||||
t.Fatal("empty base URL accepted")
|
||||
}
|
||||
if _, err := New("https://example.com", "", nil); err == nil {
|
||||
t.Fatal("empty token accepted")
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
data, _ := proto.Marshal(&api.CommonResponse{Header: &api.ResponseHeader{Code: 400, Message: "bad request", RequestId: "request-123", Replayed: true}})
|
||||
_, _ = writer.Write(data)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, _ := New(server.URL, "token", server.Client())
|
||||
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
|
||||
if err == nil {
|
||||
t.Fatal("business error was accepted")
|
||||
}
|
||||
if response.GetHeader() == nil || !response.GetHeader().Replayed || response.GetHeader().RequestId != "request-123" {
|
||||
t.Fatalf("business error response = %#v", response.GetHeader())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
dto "code.zhecent.com/open/shop-crm-agent/internal/api"
|
||||
crmClient "code.zhecent.com/open/shop-crm-agent/internal/client"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type crmAgentWriteOptions struct{ requestID string }
|
||||
|
||||
func New(version string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "shop-crm-agent",
|
||||
Short: "通过 Protobuf API 操作 Shop CRM",
|
||||
Version: version,
|
||||
Args: cobra.NoArgs,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() },
|
||||
}
|
||||
cmd.AddCommand(newCrmAgentCustomerListCmd(), newCrmAgentProjectCmd(), newCrmAgentQuotationCmd(), newCrmAgentCatalogCmd(), newCrmAgentEditCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentHTTPClient() (*crmClient.Client, error) {
|
||||
return crmClient.New(os.Getenv("LIGHTCORE_API_BASE_URL"), os.Getenv("LIGHTCORE_SHOP_CRM_AGENT_TOKEN"), nil)
|
||||
}
|
||||
|
||||
func writeCrmAgentProto(writer io.Writer, message proto.Message) error {
|
||||
data, err := (protojson.MarshalOptions{Indent: " ", EmitUnpopulated: true}).Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(writer, string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
type crmAgentHeaderResponse interface {
|
||||
proto.Message
|
||||
GetHeader() *dto.ResponseHeader
|
||||
}
|
||||
|
||||
func writeCrmAgentResult(writer io.Writer, response crmAgentHeaderResponse, commandErr error) error {
|
||||
if response != nil && response.GetHeader() != nil {
|
||||
if err := writeCrmAgentProto(writer, response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return commandErr
|
||||
}
|
||||
|
||||
func printCrmAgentResult(response crmAgentHeaderResponse, commandErr error) error {
|
||||
return writeCrmAgentResult(os.Stdout, response, commandErr)
|
||||
}
|
||||
|
||||
func bindCrmAgentWrite(cmd *cobra.Command, options *crmAgentWriteOptions) {
|
||||
cmd.Flags().StringVar(&options.requestID, "request-id", "", "8-80位幂等请求 ID")
|
||||
_ = cmd.MarkFlagRequired("request-id")
|
||||
}
|
||||
|
||||
func crmAgentCommandHeader(options crmAgentWriteOptions) *dto.CommandHeader {
|
||||
return &dto.CommandHeader{RequestId: strings.TrimSpace(options.requestID)}
|
||||
}
|
||||
|
||||
func newCrmAgentCustomerListCmd() *cobra.Command {
|
||||
var keyword string
|
||||
var page, pageSize int32
|
||||
cmd := &cobra.Command{Use: "customer-list", Short: "查询创建项目可引用的 CRM 客户", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.CustomerList(cmd.Context(), &dto.CustomerListRequest{Keyword: keyword, Page: page, PageSize: pageSize})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().StringVar(&keyword, "keyword", "", "客户姓名或手机号")
|
||||
cmd.Flags().Int32Var(&page, "page", 1, "页码")
|
||||
cmd.Flags().Int32Var(&pageSize, "page-size", 20, "每页数量")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentProjectCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "project", Short: "查询和维护 CRM 项目", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentProjectListCmd(), newCrmAgentProjectGetCmd(), newCrmAgentProjectCreateCmd(), newCrmAgentProjectUpdateCmd(), newCrmAgentProjectStatusCmd(), newCrmAgentProjectDeletePreviewCmd(), newCrmAgentProjectDeleteCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentProjectListCmd() *cobra.Command {
|
||||
var keyword, status, houseType string
|
||||
var page, pageSize int32
|
||||
cmd := &cobra.Command{Use: "list", Short: "分页查询项目", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectList(cmd.Context(), &dto.ProjectListRequest{Keyword: keyword, Status: status, HouseType: houseType, Page: page, PageSize: pageSize})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().StringVar(&keyword, "keyword", "", "项目关键词")
|
||||
cmd.Flags().StringVar(&status, "status", "", "项目状态")
|
||||
cmd.Flags().StringVar(&houseType, "house-type", "", "房屋类型")
|
||||
cmd.Flags().Int32Var(&page, "page", 1, "页码")
|
||||
cmd.Flags().Int32Var(&pageSize, "page-size", 20, "每页数量")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentProjectGetCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
cmd := &cobra.Command{Use: "get", Short: "读取项目详情", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectGet(cmd.Context(), &dto.ProjectGetRequest{ProjectId: projectID})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type crmAgentProjectFields struct {
|
||||
name string
|
||||
customerID uint64
|
||||
houseType, province, provinceCode, city, cityCode, district, districtCode, address, remark string
|
||||
}
|
||||
|
||||
func bindCrmAgentProjectFields(cmd *cobra.Command, fields *crmAgentProjectFields, includeCustomer, requireCore bool) {
|
||||
cmd.Flags().StringVar(&fields.name, "name", "", "项目名称")
|
||||
cmd.Flags().StringVar(&fields.houseType, "house-type", "", "房屋类型: flat/duplex/villa/commercial/other")
|
||||
if includeCustomer {
|
||||
cmd.Flags().Uint64Var(&fields.customerID, "customer-id", 0, "客户 ID")
|
||||
_ = cmd.MarkFlagRequired("customer-id")
|
||||
}
|
||||
cmd.Flags().StringVar(&fields.province, "province", "", "省份")
|
||||
cmd.Flags().StringVar(&fields.provinceCode, "province-code", "", "省份编码")
|
||||
cmd.Flags().StringVar(&fields.city, "city", "", "城市")
|
||||
cmd.Flags().StringVar(&fields.cityCode, "city-code", "", "城市编码")
|
||||
cmd.Flags().StringVar(&fields.district, "district", "", "区县")
|
||||
cmd.Flags().StringVar(&fields.districtCode, "district-code", "", "区县编码")
|
||||
cmd.Flags().StringVar(&fields.address, "address", "", "详细地址")
|
||||
cmd.Flags().StringVar(&fields.remark, "remark", "", "备注")
|
||||
if requireCore {
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
_ = cmd.MarkFlagRequired("house-type")
|
||||
}
|
||||
}
|
||||
|
||||
func newCrmAgentProjectCreateCmd() *cobra.Command {
|
||||
var fields crmAgentProjectFields
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "create", Short: "创建项目", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectCreate(cmd.Context(), &dto.ProjectCreateRequest{Command: crmAgentCommandHeader(write), Name: fields.name, CustomerId: fields.customerID, HouseType: fields.houseType, Province: fields.province, ProvinceCode: fields.provinceCode, City: fields.city, CityCode: fields.cityCode, District: fields.district, DistrictCode: fields.districtCode, Address: fields.address, Remark: fields.remark})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindCrmAgentProjectFields(cmd, &fields, true, true)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentProjectUpdateCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
var fields crmAgentProjectFields
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "update", Short: "更新项目资料", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectUpdate(cmd.Context(), &dto.ProjectUpdateRequest{
|
||||
Command: crmAgentCommandHeader(write), ProjectId: projectID,
|
||||
Name: crmAgentChangedString(cmd, "name", fields.name), HouseType: crmAgentChangedString(cmd, "house-type", fields.houseType),
|
||||
Province: crmAgentChangedString(cmd, "province", fields.province), ProvinceCode: crmAgentChangedString(cmd, "province-code", fields.provinceCode),
|
||||
City: crmAgentChangedString(cmd, "city", fields.city), CityCode: crmAgentChangedString(cmd, "city-code", fields.cityCode),
|
||||
District: crmAgentChangedString(cmd, "district", fields.district), DistrictCode: crmAgentChangedString(cmd, "district-code", fields.districtCode),
|
||||
Address: crmAgentChangedString(cmd, "address", fields.address),
|
||||
Remark: crmAgentChangedString(cmd, "remark", fields.remark),
|
||||
})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
bindCrmAgentProjectFields(cmd, &fields, false, false)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func crmAgentChangedString(cmd *cobra.Command, flagName, value string) *string {
|
||||
if !cmd.Flags().Changed(flagName) {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func newCrmAgentProjectStatusCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
var status string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "status", Short: "修改项目状态", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectStatus(cmd.Context(), &dto.ProjectStatusRequest{Command: crmAgentCommandHeader(write), ProjectId: projectID, Status: status})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
cmd.Flags().StringVar(&status, "status", "", "状态: active/completed/shelved")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
_ = cmd.MarkFlagRequired("status")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentProjectDeletePreviewCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
cmd := &cobra.Command{Use: "delete-preview", Short: "预览空项目删除", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectDeletePreview(cmd.Context(), &dto.ProjectDeletePreviewRequest{ProjectId: projectID})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentProjectDeleteCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
var changeSet string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "delete", Short: "删除已预览的空项目", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.ProjectDelete(cmd.Context(), &dto.ProjectDeleteRequest{Command: crmAgentCommandHeader(write), ProjectId: projectID, ChangeSetId: changeSet})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
cmd.Flags().StringVar(&changeSet, "change-set-id", "", "delete-preview 返回的 changeSetId")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
_ = cmd.MarkFlagRequired("change-set-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentQuotationCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "quotation", Short: "查询和维护独立报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentQuotationListCmd(), newCrmAgentQuotationGetCmd(), newCrmAgentQuotationCreateCmd(), newCrmAgentQuotationCopyCmd(), newCrmAgentQuotationTitleCmd(), newCrmAgentQuotationStatusCmd(), newCrmAgentQuotationLockCmd(true), newCrmAgentQuotationLockCmd(false), newCrmAgentQuotationDeletePreviewCmd(), newCrmAgentQuotationDeleteCmd())
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationListCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
var keyword, status string
|
||||
var page, pageSize int32
|
||||
cmd := &cobra.Command{Use: "list", Short: "分页查询报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationList(cmd.Context(), &dto.QuotationListRequest{ProjectId: projectID, Keyword: keyword, Status: status, Page: page, PageSize: pageSize})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
cmd.Flags().StringVar(&keyword, "keyword", "", "关键词")
|
||||
cmd.Flags().StringVar(&status, "status", "", "报价状态")
|
||||
cmd.Flags().Int32Var(&page, "page", 1, "页码")
|
||||
cmd.Flags().Int32Var(&pageSize, "page-size", 20, "每页数量")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationGetCmd() *cobra.Command {
|
||||
var id uint64
|
||||
cmd := &cobra.Command{Use: "get", Short: "读取报价详情", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationGet(cmd.Context(), &dto.QuotationGetRequest{QuotationId: id})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationCreateCmd() *cobra.Command {
|
||||
var projectID uint64
|
||||
var title string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "create", Short: "创建空报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationCreate(cmd.Context(), &dto.QuotationCreateRequest{Command: crmAgentCommandHeader(write), ProjectId: projectID, Title: title})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&projectID, "project-id", 0, "项目 ID")
|
||||
cmd.Flags().StringVar(&title, "title", "报价", "报价标题")
|
||||
_ = cmd.MarkFlagRequired("project-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationCopyCmd() *cobra.Command {
|
||||
var id uint64
|
||||
var title string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "copy", Short: "复制独立报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationCopy(cmd.Context(), &dto.QuotationCopyRequest{Command: crmAgentCommandHeader(write), QuotationId: id, Title: title})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "源报价 ID")
|
||||
cmd.Flags().StringVar(&title, "title", "", "副本标题")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
_ = cmd.MarkFlagRequired("title")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationTitleCmd() *cobra.Command {
|
||||
var id uint64
|
||||
var title string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "title", Short: "修改报价标题", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationTitle(cmd.Context(), &dto.QuotationTitleRequest{Command: crmAgentCommandHeader(write), QuotationId: id, Title: title})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
cmd.Flags().StringVar(&title, "title", "", "标题")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
_ = cmd.MarkFlagRequired("title")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationStatusCmd() *cobra.Command {
|
||||
var id uint64
|
||||
var status string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "status", Short: "修改报价状态", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationStatus(cmd.Context(), &dto.QuotationStatusRequest{Command: crmAgentCommandHeader(write), QuotationId: id, Status: status})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
cmd.Flags().StringVar(&status, "status", "", "报价状态")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
_ = cmd.MarkFlagRequired("status")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationLockCmd(locked bool) *cobra.Command {
|
||||
use, short := "unlock", "解锁报价"
|
||||
if locked {
|
||||
use, short = "lock", "锁定报价"
|
||||
}
|
||||
var id uint64
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: use, Short: short, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request := &dto.QuotationLockRequest{Command: crmAgentCommandHeader(write), QuotationId: id}
|
||||
var response *dto.QuotationResponse
|
||||
if locked {
|
||||
response, err = client.QuotationLock(cmd.Context(), request)
|
||||
} else {
|
||||
response, err = client.QuotationUnlock(cmd.Context(), request)
|
||||
}
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationDeletePreviewCmd() *cobra.Command {
|
||||
var id uint64
|
||||
cmd := &cobra.Command{Use: "delete-preview", Short: "预览报价删除", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationDeletePreview(cmd.Context(), &dto.QuotationDeletePreviewRequest{QuotationId: id})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentQuotationDeleteCmd() *cobra.Command {
|
||||
var id uint64
|
||||
var revision int32
|
||||
var changeSet string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "delete", Short: "按 revision 删除报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.QuotationDelete(cmd.Context(), &dto.QuotationDeleteRequest{Command: crmAgentCommandHeader(write), QuotationId: id, Revision: revision, ChangeSetId: changeSet})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var(&id, "quotation-id", 0, "报价 ID")
|
||||
cmd.Flags().Int32Var(&revision, "revision", 0, "delete-preview 返回的 revision")
|
||||
cmd.Flags().StringVar(&changeSet, "change-set-id", "", "delete-preview 返回的 changeSetId")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
_ = cmd.MarkFlagRequired("revision")
|
||||
_ = cmd.MarkFlagRequired("change-set-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentCatalogCmd() *cobra.Command {
|
||||
var keyword string
|
||||
var categoryID, rootCategoryID, brandID uint64
|
||||
var page, pageSize int32
|
||||
cmd := &cobra.Command{Use: "catalog-search", Short: "查询可加入报价的产品库 SKU", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.CatalogSearch(cmd.Context(), &dto.CatalogSearchRequest{Keyword: keyword, CategoryId: categoryID, RootCategoryId: rootCategoryID, BrandId: brandID, Page: page, PageSize: pageSize})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().StringVar(&keyword, "keyword", "", "关键词")
|
||||
cmd.Flags().Uint64Var(&categoryID, "category-id", 0, "分类 ID")
|
||||
cmd.Flags().Uint64Var(&rootCategoryID, "root-category-id", 0, "根分类 ID")
|
||||
cmd.Flags().Uint64Var(&brandID, "brand-id", 0, "品牌 ID")
|
||||
cmd.Flags().Int32Var(&page, "page", 1, "页码")
|
||||
cmd.Flags().Int32Var(&pageSize, "page-size", 20, "每页数量")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
dto "code.zhecent.com/open/shop-crm-agent/internal/api"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func TestRootCommandExposesOnlyCrmOperations(t *testing.T) {
|
||||
cmd := New("test-version")
|
||||
want := map[string]bool{
|
||||
"customer-list": true, "project": true,
|
||||
"quotation": true, "catalog-search": true, "edit": true,
|
||||
}
|
||||
for _, child := range cmd.Commands() {
|
||||
if child.Name() == "credential" {
|
||||
t.Fatal("external client exposes credential administration")
|
||||
}
|
||||
if child.Name() == "progress-template-list" {
|
||||
t.Fatal("external client exposes retired progress template query")
|
||||
}
|
||||
delete(want, child.Name())
|
||||
}
|
||||
if len(want) != 0 {
|
||||
t.Fatalf("missing root commands = %v", want)
|
||||
}
|
||||
if cmd.Version != "test-version" {
|
||||
t.Fatalf("version = %q", cmd.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectCommandsExcludeProgressAndTemplateFlag(t *testing.T) {
|
||||
root := New("test-version")
|
||||
project, _, err := root.Find([]string{"project"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, child := range project.Commands() {
|
||||
if child.Name() == "progress-complete" {
|
||||
t.Fatal("project command exposes retired progress completion")
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"create", "update"} {
|
||||
command, _, findErr := root.Find([]string{"project", name})
|
||||
if findErr != nil {
|
||||
t.Fatal(findErr)
|
||||
}
|
||||
if command.Flags().Lookup("progress-template-key") != nil {
|
||||
t.Fatalf("project %s exposes retired progress template flag", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteCommandRequiresEnvironmentConfiguration(t *testing.T) {
|
||||
t.Setenv("LIGHTCORE_API_BASE_URL", "")
|
||||
t.Setenv("LIGHTCORE_SHOP_CRM_AGENT_TOKEN", "")
|
||||
cmd := New("test")
|
||||
cmd.SetArgs([]string{"customer-list"})
|
||||
if err := cmd.Execute(); err == nil {
|
||||
t.Fatal("missing environment configuration was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCrmAgentResultPrintsStructuredBusinessError(t *testing.T) {
|
||||
commandErr := errors.New("CRM Agent API 错误 400: 报价已锁定")
|
||||
response := &dto.ProjectResponse{Header: &dto.ResponseHeader{
|
||||
Code: 400, Message: "报价已锁定", RequestId: "request-123", Replayed: true,
|
||||
}}
|
||||
var output bytes.Buffer
|
||||
err := writeCrmAgentResult(&output, response, commandErr)
|
||||
if !errors.Is(err, commandErr) {
|
||||
t.Fatalf("command error = %v", err)
|
||||
}
|
||||
printed := &dto.ProjectResponse{}
|
||||
if err := protojson.Unmarshal(output.Bytes(), printed); err != nil {
|
||||
t.Fatalf("decode output: %v", err)
|
||||
}
|
||||
if printed.GetHeader() == nil || printed.GetHeader().Code != 400 || printed.GetHeader().RequestId != "request-123" || !printed.GetHeader().Replayed {
|
||||
t.Fatalf("printed header = %#v", printed.GetHeader())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCrmAgentResultSkipsMissingTransportResponse(t *testing.T) {
|
||||
commandErr := errors.New("connection refused")
|
||||
var output bytes.Buffer
|
||||
err := writeCrmAgentResult(&output, &dto.ProjectResponse{}, commandErr)
|
||||
if !errors.Is(err, commandErr) || output.Len() != 0 {
|
||||
t.Fatalf("error=%v output=%q", err, output.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
dto "code.zhecent.com/open/shop-crm-agent/internal/api"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCrmAgentEditCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "edit", Short: "通过服务端会话编辑报价", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentEditBeginCmd(), newCrmAgentEditPreviewCmd(), newCrmAgentEditCommitCmd(), newCrmAgentEditDiscardCmd(), newCrmAgentEditFloorCmd(), newCrmAgentEditSpaceCmd(), newCrmAgentEditCategoryCmd(), newCrmAgentEditLineCmd(), newCrmAgentEditFeeCmd(), newCrmAgentEditPricingCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentEditBeginCmd() *cobra.Command {
|
||||
var quotationID uint64
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "begin", Short: "开始报价编辑会话", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditBegin(cmd.Context(), &dto.QuotationEditBeginRequest{Command: crmAgentCommandHeader(write), QuotationId: quotationID})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
cmd.Flags().Uint64Var("ationID, "quotation-id", 0, "报价 ID")
|
||||
_ = cmd.MarkFlagRequired("quotation-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentEditPreviewCmd() *cobra.Command {
|
||||
var sessionID string
|
||||
cmd := &cobra.Command{Use: "preview", Short: "预览编辑并生成 changeSetId", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditPreview(cmd.Context(), &dto.QuotationEditPreviewRequest{SessionId: sessionID})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentEditCommitCmd() *cobra.Command {
|
||||
var sessionID, changeSet string
|
||||
var revision int32
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "commit", Short: "按 revision 和 changeSetId 提交编辑", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditCommit(cmd.Context(), &dto.QuotationEditCommitRequest{Command: crmAgentCommandHeader(write), SessionId: sessionID, Revision: revision, ChangeSetId: changeSet})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
cmd.Flags().Int32Var(&revision, "revision", 0, "preview 返回的 revision")
|
||||
cmd.Flags().StringVar(&changeSet, "change-set-id", "", "preview 返回的 changeSetId")
|
||||
_ = cmd.MarkFlagRequired("revision")
|
||||
_ = cmd.MarkFlagRequired("change-set-id")
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentEditDiscardCmd() *cobra.Command {
|
||||
var sessionID string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "discard", Short: "丢弃编辑会话", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditDiscard(cmd.Context(), &dto.QuotationEditDiscardRequest{Command: crmAgentCommandHeader(write), SessionId: sessionID})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func bindSessionID(cmd *cobra.Command, value *string) {
|
||||
cmd.Flags().StringVar(value, "session-id", "", "报价编辑会话 ID")
|
||||
_ = cmd.MarkFlagRequired("session-id")
|
||||
}
|
||||
|
||||
func newCrmAgentEditMutationCmd(use, short string, bind func(*cobra.Command), build func() *dto.QuotationEditCommandRequest) *cobra.Command {
|
||||
var sessionID string
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: use, Short: short, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
request := build()
|
||||
if request == nil {
|
||||
return errors.New("编辑命令构造失败")
|
||||
}
|
||||
request.Command, request.SessionId = crmAgentCommandHeader(write), sessionID
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditCommand(cmd.Context(), request)
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
if bind != nil {
|
||||
bind(cmd)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCrmAgentEditFloorCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "floor", Short: "增改排序和移除楼层", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentFloorAddCmd(), newCrmAgentFloorRenameCmd(), newCrmAgentFloorReorderCmd(), newCrmAgentFloorRemoveCmd())
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentFloorAddCmd() *cobra.Command {
|
||||
var id, name string
|
||||
var index int32 = -1
|
||||
return newCrmAgentEditMutationCmd("add", "添加楼层", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "id", "", "可选稳定楼层 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "楼层名称")
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "插入索引,-1追加")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddFloor{AddFloor: &dto.AddFloor{Id: id, Name: name, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentFloorRenameCmd() *cobra.Command {
|
||||
var id, name string
|
||||
return newCrmAgentEditMutationCmd("rename", "重命名楼层", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "新名称")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RenameFloor{RenameFloor: &dto.RenameFloor{FloorId: id, Name: name}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentFloorReorderCmd() *cobra.Command {
|
||||
var id string
|
||||
var index int32
|
||||
return newCrmAgentEditMutationCmd("reorder", "调整楼层顺序", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().Int32Var(&index, "index", 0, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_ReorderFloor{ReorderFloor: &dto.ReorderFloor{FloorId: id, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentFloorRemoveCmd() *cobra.Command {
|
||||
var id string
|
||||
var cascade bool
|
||||
return newCrmAgentEditMutationCmd("remove", "移除楼层", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().BoolVar(&cascade, "cascade", false, "同时移除下属空间、分类和产品行")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RemoveFloor{RemoveFloor: &dto.RemoveFloor{FloorId: id, Cascade: cascade}}}
|
||||
})
|
||||
}
|
||||
|
||||
func newCrmAgentEditSpaceCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "space", Short: "增改移动排序和移除空间", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentSpaceAddCmd(), newCrmAgentSpaceRenameCmd(), newCrmAgentSpaceMoveCmd(), newCrmAgentSpaceReorderCmd(), newCrmAgentSpaceRemoveCmd())
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentSpaceAddCmd() *cobra.Command {
|
||||
var id, floorID, name string
|
||||
var index int32 = -1
|
||||
return newCrmAgentEditMutationCmd("add", "添加空间", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "id", "", "可选稳定空间 ID")
|
||||
cmd.Flags().StringVar(&floorID, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "空间名称")
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "插入索引")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddSpace{AddSpace: &dto.AddSpace{Id: id, FloorId: floorID, Name: name, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentSpaceRenameCmd() *cobra.Command {
|
||||
var id, name string
|
||||
return newCrmAgentEditMutationCmd("rename", "重命名空间", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "space-id", "", "空间 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "新名称")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RenameSpace{RenameSpace: &dto.RenameSpace{SpaceId: id, Name: name}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentSpaceMoveCmd() *cobra.Command {
|
||||
var id, floorID string
|
||||
var index int32 = -1
|
||||
return newCrmAgentEditMutationCmd("move", "移动空间到其他楼层", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "space-id", "", "空间 ID")
|
||||
cmd.Flags().StringVar(&floorID, "target-floor-id", "", "目标楼层 ID")
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
_ = cmd.MarkFlagRequired("target-floor-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_MoveSpace{MoveSpace: &dto.MoveSpace{SpaceId: id, TargetFloorId: floorID, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentSpaceReorderCmd() *cobra.Command {
|
||||
var id string
|
||||
var index int32
|
||||
return newCrmAgentEditMutationCmd("reorder", "调整空间顺序", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "space-id", "", "空间 ID")
|
||||
cmd.Flags().Int32Var(&index, "index", 0, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_ReorderSpace{ReorderSpace: &dto.ReorderSpace{SpaceId: id, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentSpaceRemoveCmd() *cobra.Command {
|
||||
var id string
|
||||
var cascade bool
|
||||
return newCrmAgentEditMutationCmd("remove", "移除空间", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "space-id", "", "空间 ID")
|
||||
cmd.Flags().BoolVar(&cascade, "cascade", false, "同时移除分类和产品行")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RemoveSpace{RemoveSpace: &dto.RemoveSpace{SpaceId: id, Cascade: cascade}}}
|
||||
})
|
||||
}
|
||||
|
||||
func newCrmAgentEditCategoryCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "category", Short: "增改排序和移除分类", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentCategoryAddCmd(), newCrmAgentCategoryRenameCmd(), newCrmAgentCategoryReorderCmd(), newCrmAgentCategoryRemoveCmd())
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentCategoryAddCmd() *cobra.Command {
|
||||
var id, floorID, spaceID, name, source, sourceKey string
|
||||
var index int32 = -1
|
||||
return newCrmAgentEditMutationCmd("add", "添加分类", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "id", "", "可选稳定分类 ID")
|
||||
cmd.Flags().StringVar(&floorID, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().StringVar(&spaceID, "space-id", "", "空间 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "分类名称")
|
||||
cmd.Flags().StringVar(&source, "source", "custom", "来源")
|
||||
cmd.Flags().StringVar(&sourceKey, "source-key", "", "来源 key")
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "插入索引")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddCategory{AddCategory: &dto.AddCategory{Id: id, FloorId: floorID, SpaceId: spaceID, Name: name, Source: source, SourceKey: sourceKey, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentCategoryRenameCmd() *cobra.Command {
|
||||
var id, name string
|
||||
return newCrmAgentEditMutationCmd("rename", "重命名分类", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "category-id", "", "分类 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "新名称")
|
||||
_ = cmd.MarkFlagRequired("category-id")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RenameCategory{RenameCategory: &dto.RenameCategory{CategoryId: id, Name: name}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentCategoryReorderCmd() *cobra.Command {
|
||||
var id string
|
||||
var index int32
|
||||
return newCrmAgentEditMutationCmd("reorder", "调整分类顺序", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "category-id", "", "分类 ID")
|
||||
cmd.Flags().Int32Var(&index, "index", 0, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("category-id")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_ReorderCategory{ReorderCategory: &dto.ReorderCategory{CategoryId: id, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentCategoryRemoveCmd() *cobra.Command {
|
||||
var id string
|
||||
var cascade bool
|
||||
return newCrmAgentEditMutationCmd("remove", "移除分类", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&id, "category-id", "", "分类 ID")
|
||||
cmd.Flags().BoolVar(&cascade, "cascade", false, "同时移除产品行")
|
||||
_ = cmd.MarkFlagRequired("category-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RemoveCategory{RemoveCategory: &dto.RemoveCategory{CategoryId: id, Cascade: cascade}}}
|
||||
})
|
||||
}
|
||||
|
||||
func newCrmAgentEditLineCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "line", Short: "增改移动排序启停和移除产品行", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentLineAddCatalogCmd(), newCrmAgentLineAddCustomCmd(), newCrmAgentLineUpdateCmd(), newCrmAgentLineMoveCmd(), newCrmAgentLineReorderCmd(), newCrmAgentLineActiveCmd(true), newCrmAgentLineActiveCmd(false), newCrmAgentLineRemoveCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
type crmAgentLineTarget struct {
|
||||
floorID, spaceID, categoryID string
|
||||
index int32
|
||||
}
|
||||
|
||||
func bindLineTarget(cmd *cobra.Command, target *crmAgentLineTarget) {
|
||||
cmd.Flags().StringVar(&target.floorID, "floor-id", "", "楼层 ID")
|
||||
cmd.Flags().StringVar(&target.spaceID, "space-id", "", "空间 ID")
|
||||
cmd.Flags().StringVar(&target.categoryID, "category-id", "", "分类 ID")
|
||||
cmd.Flags().Int32Var(&target.index, "index", -1, "分类内索引,-1追加")
|
||||
_ = cmd.MarkFlagRequired("floor-id")
|
||||
_ = cmd.MarkFlagRequired("space-id")
|
||||
_ = cmd.MarkFlagRequired("category-id")
|
||||
}
|
||||
func newCrmAgentLineAddCatalogCmd() *cobra.Command {
|
||||
var target crmAgentLineTarget
|
||||
var skuID uint64
|
||||
var quantity float64 = 1
|
||||
var discountRate int32 = 100
|
||||
var discountAmount int64
|
||||
var remark string
|
||||
return newCrmAgentEditMutationCmd("add-catalog", "按 SKU 添加产品库行", func(cmd *cobra.Command) {
|
||||
bindLineTarget(cmd, &target)
|
||||
cmd.Flags().Uint64Var(&skuID, "sku-id", 0, "产品库 SKU ID")
|
||||
cmd.Flags().Float64Var(&quantity, "quantity", 1, "数量")
|
||||
cmd.Flags().Int32Var(&discountRate, "discount-rate", 100, "折扣率0-100")
|
||||
cmd.Flags().Int64Var(&discountAmount, "discount-amount", 0, "优惠金额(分)")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "备注")
|
||||
_ = cmd.MarkFlagRequired("sku-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddCatalogLine{AddCatalogLine: &dto.AddCatalogLine{FloorId: target.floorID, SpaceId: target.spaceID, CategoryId: target.categoryID, SkuId: skuID, Quantity: quantity, DiscountRate: discountRate, DiscountAmount: discountAmount, Remark: remark, Index: target.index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentLineAddCustomCmd() *cobra.Command {
|
||||
var target crmAgentLineTarget
|
||||
var name, spec, remark string
|
||||
var quantity float64 = 1
|
||||
var unitPrice, costPrice, discountAmount int64
|
||||
var discountRate int32 = 100
|
||||
return newCrmAgentEditMutationCmd("add-custom", "添加自定义产品行", func(cmd *cobra.Command) {
|
||||
bindLineTarget(cmd, &target)
|
||||
cmd.Flags().StringVar(&name, "name", "", "产品名称")
|
||||
cmd.Flags().StringVar(&spec, "spec", "", "规格")
|
||||
cmd.Flags().Float64Var(&quantity, "quantity", 1, "数量")
|
||||
cmd.Flags().Int64Var(&unitPrice, "unit-price", 0, "售价(分)")
|
||||
cmd.Flags().Int64Var(&costPrice, "cost-price", 0, "成本(分)")
|
||||
cmd.Flags().Int32Var(&discountRate, "discount-rate", 100, "折扣率0-100")
|
||||
cmd.Flags().Int64Var(&discountAmount, "discount-amount", 0, "优惠金额(分)")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "备注")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddCustomLine{AddCustomLine: &dto.AddCustomLine{FloorId: target.floorID, SpaceId: target.spaceID, CategoryId: target.categoryID, GoodsName: name, SpecCombination: spec, Quantity: quantity, UnitPrice: unitPrice, CostPrice: costPrice, DiscountRate: discountRate, DiscountAmount: discountAmount, Remark: remark, Index: target.index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentLineUpdateCmd() *cobra.Command {
|
||||
var sessionID, lineID, name, spec, remark string
|
||||
var quantity float64
|
||||
var unitPrice, costPrice, discountAmount int64
|
||||
var discountRate int32
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "update", Short: "按已提供 flags 更新产品行", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
update := &dto.UpdateLine{LineId: lineID}
|
||||
if cmd.Flags().Changed("name") {
|
||||
update.GoodsName = &name
|
||||
}
|
||||
if cmd.Flags().Changed("spec") {
|
||||
update.SpecCombination = &spec
|
||||
}
|
||||
if cmd.Flags().Changed("quantity") {
|
||||
update.Quantity = &quantity
|
||||
}
|
||||
if cmd.Flags().Changed("unit-price") {
|
||||
update.UnitPrice = &unitPrice
|
||||
}
|
||||
if cmd.Flags().Changed("cost-price") {
|
||||
update.CostPrice = &costPrice
|
||||
}
|
||||
if cmd.Flags().Changed("discount-rate") {
|
||||
update.DiscountRate = &discountRate
|
||||
}
|
||||
if cmd.Flags().Changed("discount-amount") {
|
||||
update.DiscountAmount = &discountAmount
|
||||
}
|
||||
if cmd.Flags().Changed("remark") {
|
||||
update.Remark = &remark
|
||||
}
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditCommand(cmd.Context(), &dto.QuotationEditCommandRequest{Command: crmAgentCommandHeader(write), SessionId: sessionID, Edit: &dto.QuotationEditCommandRequest_UpdateLine{UpdateLine: update}})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
cmd.Flags().StringVar(&lineID, "line-id", "", "产品行 ID")
|
||||
cmd.Flags().StringVar(&name, "name", "", "产品名称(仅自定义行)")
|
||||
cmd.Flags().StringVar(&spec, "spec", "", "规格(仅自定义行)")
|
||||
cmd.Flags().Float64Var(&quantity, "quantity", 0, "数量")
|
||||
cmd.Flags().Int64Var(&unitPrice, "unit-price", 0, "售价(分)")
|
||||
cmd.Flags().Int64Var(&costPrice, "cost-price", 0, "成本(分,仅自定义行)")
|
||||
cmd.Flags().Int32Var(&discountRate, "discount-rate", 0, "折扣率0-100")
|
||||
cmd.Flags().Int64Var(&discountAmount, "discount-amount", 0, "优惠金额(分)")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "备注")
|
||||
_ = cmd.MarkFlagRequired("line-id")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentLineMoveCmd() *cobra.Command {
|
||||
var target crmAgentLineTarget
|
||||
var lineID string
|
||||
return newCrmAgentEditMutationCmd("move", "移动产品行", func(cmd *cobra.Command) {
|
||||
bindLineTarget(cmd, &target)
|
||||
cmd.Flags().StringVar(&lineID, "line-id", "", "产品行 ID")
|
||||
_ = cmd.MarkFlagRequired("line-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_MoveLine{MoveLine: &dto.MoveLine{LineId: lineID, FloorId: target.floorID, SpaceId: target.spaceID, CategoryId: target.categoryID, Index: target.index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentLineReorderCmd() *cobra.Command {
|
||||
var lineID string
|
||||
var index int32
|
||||
return newCrmAgentEditMutationCmd("reorder", "调整产品行在分类内的顺序", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&lineID, "line-id", "", "产品行 ID")
|
||||
cmd.Flags().Int32Var(&index, "index", 0, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("line-id")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_ReorderLine{ReorderLine: &dto.ReorderLine{LineId: lineID, Index: index}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentLineActiveCmd(active bool) *cobra.Command {
|
||||
use, short := "disable", "停用产品行"
|
||||
if active {
|
||||
use, short = "enable", "启用产品行"
|
||||
}
|
||||
var lineID string
|
||||
return newCrmAgentEditMutationCmd(use, short, func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&lineID, "line-id", "", "产品行 ID")
|
||||
_ = cmd.MarkFlagRequired("line-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_SetLineActive{SetLineActive: &dto.SetLineActive{LineId: lineID, Active: active}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentLineRemoveCmd() *cobra.Command {
|
||||
var lineID string
|
||||
return newCrmAgentEditMutationCmd("remove", "移除产品行", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&lineID, "line-id", "", "产品行 ID")
|
||||
_ = cmd.MarkFlagRequired("line-id")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RemoveLine{RemoveLine: &dto.RemoveLine{LineId: lineID}}}
|
||||
})
|
||||
}
|
||||
|
||||
func newCrmAgentEditFeeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "fee", Short: "增改排序和移除费用", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }}
|
||||
cmd.AddCommand(newCrmAgentFeeAddCmd(), newCrmAgentFeeUpdateCmd(), newCrmAgentFeeReorderCmd(), newCrmAgentFeeRemoveCmd())
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentFeeAddCmd() *cobra.Command {
|
||||
var name, feeType, remark string
|
||||
var fixedAmount int64
|
||||
var percentage, index int32
|
||||
index = -1
|
||||
return newCrmAgentEditMutationCmd("add", "添加费用", func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&name, "name", "", "费用名称")
|
||||
cmd.Flags().StringVar(&feeType, "type", "fixed", "fixed/discounted_percent/original_percent")
|
||||
cmd.Flags().Int64Var(&fixedAmount, "fixed-amount", 0, "固定金额(分)")
|
||||
cmd.Flags().Int32Var(&percentage, "percentage", 0, "百分比0-100")
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "插入索引")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "备注")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_AddFee{AddFee: &dto.AddFee{Name: name, Type: feeType, FixedAmount: fixedAmount, Percentage: percentage, Index: index, Remark: remark}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentFeeUpdateCmd() *cobra.Command {
|
||||
var sessionID, name, feeType, remark string
|
||||
var index, percentage int32
|
||||
var fixedAmount int64
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "update", Short: "按已提供 flags 更新费用", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
update := &dto.UpdateFee{Index: index}
|
||||
if cmd.Flags().Changed("name") {
|
||||
update.Name = &name
|
||||
}
|
||||
if cmd.Flags().Changed("type") {
|
||||
update.Type = &feeType
|
||||
}
|
||||
if cmd.Flags().Changed("fixed-amount") {
|
||||
update.FixedAmount = &fixedAmount
|
||||
}
|
||||
if cmd.Flags().Changed("percentage") {
|
||||
update.Percentage = &percentage
|
||||
}
|
||||
if cmd.Flags().Changed("remark") {
|
||||
update.Remark = &remark
|
||||
}
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditCommand(cmd.Context(), &dto.QuotationEditCommandRequest{Command: crmAgentCommandHeader(write), SessionId: sessionID, Edit: &dto.QuotationEditCommandRequest_UpdateFee{UpdateFee: update}})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "费用索引")
|
||||
cmd.Flags().StringVar(&name, "name", "", "名称")
|
||||
cmd.Flags().StringVar(&feeType, "type", "", "费用类型")
|
||||
cmd.Flags().Int64Var(&fixedAmount, "fixed-amount", 0, "固定金额(分)")
|
||||
cmd.Flags().Int32Var(&percentage, "percentage", 0, "百分比0-100")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "备注")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
return cmd
|
||||
}
|
||||
func newCrmAgentFeeReorderCmd() *cobra.Command {
|
||||
var index, target int32
|
||||
return newCrmAgentEditMutationCmd("reorder", "调整费用顺序", func(cmd *cobra.Command) {
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "当前索引")
|
||||
cmd.Flags().Int32Var(&target, "target-index", 0, "目标索引")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
_ = cmd.MarkFlagRequired("target-index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_ReorderFee{ReorderFee: &dto.ReorderFee{Index: index, TargetIndex: target}}}
|
||||
})
|
||||
}
|
||||
func newCrmAgentFeeRemoveCmd() *cobra.Command {
|
||||
var index int32
|
||||
return newCrmAgentEditMutationCmd("remove", "移除费用", func(cmd *cobra.Command) {
|
||||
cmd.Flags().Int32Var(&index, "index", -1, "费用索引")
|
||||
_ = cmd.MarkFlagRequired("index")
|
||||
}, func() *dto.QuotationEditCommandRequest {
|
||||
return &dto.QuotationEditCommandRequest{Edit: &dto.QuotationEditCommandRequest_RemoveFee{RemoveFee: &dto.RemoveFee{Index: index}}}
|
||||
})
|
||||
}
|
||||
|
||||
func newCrmAgentEditPricingCmd() *cobra.Command {
|
||||
var sessionID, remark string
|
||||
var discountRate int32
|
||||
var discountAmount, actualAmount int64
|
||||
var adjusted bool
|
||||
var write crmAgentWriteOptions
|
||||
cmd := &cobra.Command{Use: "pricing", Short: "修改整单折扣、优惠、实际金额或调价标记", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
pricing := &dto.SetPricing{}
|
||||
if cmd.Flags().Changed("discount-rate") {
|
||||
pricing.DiscountRate = &discountRate
|
||||
}
|
||||
if cmd.Flags().Changed("discount-amount") {
|
||||
pricing.DiscountAmount = &discountAmount
|
||||
}
|
||||
if cmd.Flags().Changed("actual-amount") {
|
||||
pricing.ActualAmount = &actualAmount
|
||||
}
|
||||
if cmd.Flags().Changed("adjusted") {
|
||||
pricing.HasPriceAdjustment = &adjusted
|
||||
}
|
||||
if cmd.Flags().Changed("remark") {
|
||||
pricing.Remark = &remark
|
||||
}
|
||||
client, err := newCrmAgentHTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.EditCommand(cmd.Context(), &dto.QuotationEditCommandRequest{Command: crmAgentCommandHeader(write), SessionId: sessionID, Edit: &dto.QuotationEditCommandRequest_SetPricing{SetPricing: pricing}})
|
||||
return printCrmAgentResult(response, err)
|
||||
}}
|
||||
bindSessionID(cmd, &sessionID)
|
||||
bindCrmAgentWrite(cmd, &write)
|
||||
cmd.Flags().Int32Var(&discountRate, "discount-rate", 0, "整单折扣率0-100")
|
||||
cmd.Flags().Int64Var(&discountAmount, "discount-amount", 0, "整单优惠金额(分)")
|
||||
cmd.Flags().Int64Var(&actualAmount, "actual-amount", 0, "实际成交金额(分)")
|
||||
cmd.Flags().BoolVar(&adjusted, "adjusted", false, "是否标记人工调价")
|
||||
cmd.Flags().StringVar(&remark, "remark", "", "报价备注")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const modulePath = "code.zhecent.com/open/shop-crm-agent"
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
repoRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(repoRoot, "go.mod")); err != nil {
|
||||
return errors.New("build.go must run from the shop-crm-agent repository root")
|
||||
}
|
||||
moduleCommand := exec.Command("go", "list", "-m", "-f", "{{.Path}}")
|
||||
moduleCommand.Dir = repoRoot
|
||||
moduleOutput, err := moduleCommand.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read Go module path: %w: %s", err, strings.TrimSpace(string(moduleOutput)))
|
||||
}
|
||||
if actualModule := strings.TrimSpace(string(moduleOutput)); actualModule != modulePath {
|
||||
return fmt.Errorf("unexpected Go module; want %s", modulePath)
|
||||
}
|
||||
|
||||
env := withEnv(os.Environ(), "CGO_ENABLED", "0")
|
||||
if err := runCommand(repoRoot, env, "go", "test", "./..."); err != nil {
|
||||
return fmt.Errorf("test shop-crm-agent: %w", err)
|
||||
}
|
||||
|
||||
version := gitVersion(repoRoot)
|
||||
binaryName := "shop-crm-agent"
|
||||
if runtime.GOOS == "windows" {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
binDir := filepath.Join(repoRoot, "bin")
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := os.CreateTemp(binDir, "."+binaryName+".tmp-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
if err := tempFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(tempPath); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
ldflags := "-s -w -X main.version=" + version
|
||||
if err := runCommand(repoRoot, env, "go", "build", "-trimpath", "-ldflags", ldflags, "-o", tempPath, "./cmd/shop-crm-agent"); err != nil {
|
||||
return fmt.Errorf("build shop-crm-agent: %w", err)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := os.Chmod(tempPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
probe := exec.Command(tempPath, "--version")
|
||||
probe.Dir = repoRoot
|
||||
probeOutput, err := probe.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("probe built binary: %w: %s", err, strings.TrimSpace(string(probeOutput)))
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(binDir, binaryName)
|
||||
if err := replaceBinary(tempPath, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("installed: %s\n", filepath.Join("bin", binaryName))
|
||||
fmt.Printf("version: %s\n", strings.TrimSpace(string(probeOutput)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitVersion(repoRoot string) string {
|
||||
command := exec.Command("git", "describe", "--tags", "--always", "--dirty")
|
||||
command.Dir = repoRoot
|
||||
output, err := command.Output()
|
||||
if err != nil || strings.TrimSpace(string(output)) == "" {
|
||||
return "dev"
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
func replaceBinary(source, target string) error {
|
||||
if err := os.Rename(source, target); err != nil {
|
||||
return fmt.Errorf("install built binary: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCommand(dir string, env []string, name string, args ...string) error {
|
||||
command := exec.Command(name, args...)
|
||||
command.Dir = dir
|
||||
command.Env = env
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
return command.Run()
|
||||
}
|
||||
|
||||
func withEnv(env []string, key, value string) []string {
|
||||
prefix := key + "="
|
||||
result := make([]string, 0, len(env)+1)
|
||||
for _, entry := range env {
|
||||
if !strings.HasPrefix(entry, prefix) {
|
||||
result = append(result, entry)
|
||||
}
|
||||
}
|
||||
return append(result, prefix+value)
|
||||
}
|
||||
Reference in New Issue
Block a user