原来的采购任务页是骨架:var rows []gin.H 从不查库,页面永远为空; TODO 写的还是只查 task_type = 'purchase'。所以 #18 建出来的采集任务 在界面上哪儿都看不到——用户点完「创建采集任务」只能盯着 collect_status 猜。 模块名定为「采集采购」(用户指定),直接点出这页装的是哪两类任务, 比泛称「任务」更能让人一眼知道点进去看什么。路由 /tasks 不变, 改路由会让已有书签和文档链接全失效,没有收益。 列不按类型并列——两种任务字段完全不同,并列会让采集任务行一半是空列。 改成固定列 + 一列「目标」把业务信息概括成一句话: 采集 PDD 737116531267 采购 SO-001 · M/黑色 · 2件 · ≤¥42.00 拼接逻辑在 service 层,模板只负责显示。 统计和列表共用同一个筛选条件拼装函数。分开写的话总有一天会忘了 给统计也加条件,数字和表格对不上,操作员会以为页面坏了。 无主任务的客户端列显示「—」。#17 之后采集任务默认无主,这列会大量为空。 详情弹窗只读,采购专有字段(数量、价格上限、目标规格)在采集任务里 整段不出现,不显示空行。复用 #18 的弹窗机制,app.js 无需改动。 顺带清掉 PDD 页加进来之后一直没跟上的模块计数:多处「四个模块/四个页面」 改成五个。其中 06-quality-security.md 那两处是验证清单, 照着做的人只会测四个页面,PDD 页永远不在回归范围里。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
456 lines
16 KiB
Go
456 lines
16 KiB
Go
package service
|
||
|
||
import (
|
||
"database/sql"
|
||
"testing"
|
||
|
||
"cmautobuy/admin/model"
|
||
"cmautobuy/admin/repository"
|
||
)
|
||
|
||
// insertTestTask 插一条任务,字段按需覆盖默认值,供本文件的用例使用。
|
||
// 跟 client_test.go 里的 insertTask 不一样:那个专为"领取"场景写死了固定值,
|
||
// 这里需要覆盖类型、状态、options 等更多维度。
|
||
type testTaskParams struct {
|
||
taskID string
|
||
taskType model.TaskType
|
||
status model.TaskStatus
|
||
assignedClient string
|
||
orderNo string
|
||
pddGoodsID string
|
||
pddOptions string
|
||
quantity int
|
||
maxPriceCent int64
|
||
errorCode string
|
||
errorMessage string
|
||
resultData string
|
||
claimedAt string
|
||
finishedAt string
|
||
}
|
||
|
||
func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
|
||
t.Helper()
|
||
now := model.NowISO()
|
||
|
||
var assigned, orderNo, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
|
||
if p.assignedClient != "" {
|
||
assigned = p.assignedClient
|
||
}
|
||
if p.orderNo != "" {
|
||
orderNo = p.orderNo
|
||
}
|
||
if p.pddGoodsID != "" {
|
||
pddGoodsID = p.pddGoodsID
|
||
}
|
||
if p.pddOptions != "" {
|
||
pddOptions = p.pddOptions
|
||
}
|
||
if p.errorCode != "" {
|
||
errCode = p.errorCode
|
||
}
|
||
if p.errorMessage != "" {
|
||
errMsg = p.errorMessage
|
||
}
|
||
if p.resultData != "" {
|
||
resultData = p.resultData
|
||
}
|
||
if p.claimedAt != "" {
|
||
claimedAt = p.claimedAt
|
||
}
|
||
if p.finishedAt != "" {
|
||
finishedAt = p.finishedAt
|
||
}
|
||
|
||
var quantity, maxPrice any
|
||
if p.quantity > 0 {
|
||
quantity = p.quantity
|
||
}
|
||
if p.maxPriceCent > 0 {
|
||
maxPrice = p.maxPriceCent
|
||
}
|
||
|
||
_, err := db.Exec(`
|
||
INSERT INTO tasks (task_id, task_type, status, assigned_client, claimed_at,
|
||
order_no, pdd_goods_url, pdd_goods_id, pdd_options,
|
||
quantity, max_price_cent,
|
||
error_code, error_message, result_data, finished_at,
|
||
created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
|
||
?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
p.taskID, string(p.taskType), string(p.status), assigned, claimedAt,
|
||
orderNo, pddGoodsID, pddOptions,
|
||
quantity, maxPrice,
|
||
errCode, errMsg, resultData, finishedAt,
|
||
now, now)
|
||
if err != nil {
|
||
t.Fatalf("插入测试任务 %s 失败: %v", p.taskID, err)
|
||
}
|
||
}
|
||
|
||
// ── 列表:同时显示采集和采购 ──────────────────────────
|
||
|
||
func TestListTasksView_同时显示采集和采购(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
|
||
|
||
result, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 2 {
|
||
t.Fatalf("应该看到 2 条任务,实际 %d 条", len(result.Rows))
|
||
}
|
||
}
|
||
|
||
// ── 类型 / 状态 / 关键词筛选 ──────────────────────────
|
||
|
||
func TestListTasksView_类型筛选生效(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
|
||
|
||
result, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskCollect})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 || result.Rows[0].TaskID != "COL-1" {
|
||
t.Fatalf("筛选采集应该只剩 COL-1,实际 %+v", result.Rows)
|
||
}
|
||
if !result.IsFiltered {
|
||
t.Error("有类型筛选时 IsFiltered 应该为 true")
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_状态筛选生效(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "222"})
|
||
|
||
result, err := ListTasksView(db, repository.TaskFilter{Status: model.TaskSucceeded})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 || result.Rows[0].TaskID != "COL-2" {
|
||
t.Fatalf("筛选'成功'应该只剩 COL-2,实际 %+v", result.Rows)
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_关键词同时匹配任务编号订单号PDD商品ID(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-ABC", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-XYZ"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-2", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-OTHER"})
|
||
|
||
// 按任务编号命中
|
||
r, err := ListTasksView(db, repository.TaskFilter{Keyword: "ABC"})
|
||
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-ABC" {
|
||
t.Fatalf("按任务编号搜索失败: err=%v rows=%+v", err, r)
|
||
}
|
||
// 按订单号命中
|
||
r, err = ListTasksView(db, repository.TaskFilter{Keyword: "XYZ"})
|
||
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "PUR-1" {
|
||
t.Fatalf("按订单号搜索失败: err=%v rows=%+v", err, r)
|
||
}
|
||
// 按 PDD 商品 ID 命中
|
||
r, err = ListTasksView(db, repository.TaskFilter{Keyword: "737116531267"})
|
||
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-ABC" {
|
||
t.Fatalf("按 PDD 商品 ID 搜索失败: err=%v rows=%+v", err, r)
|
||
}
|
||
}
|
||
|
||
// ── 目标列 ────────────────────────────────────────────
|
||
|
||
func TestListTasksView_目标列_采集任务显示PDD商品ID(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
want := "PDD 737116531267"
|
||
if r.Rows[0].Target != want {
|
||
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_目标列_采集任务能join到标题时追加显示(t *testing.T) {
|
||
db := newTestDB(t)
|
||
if _, err := db.Exec(`
|
||
INSERT INTO pdd_products (goods_id, url, title, collect_status, created_at, updated_at)
|
||
VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
|
||
'秋冬保暖加绒卫衣', 'collected', ?, ?)`, model.NowISO(), model.NowISO()); err != nil {
|
||
t.Fatalf("插入 PDD 商品失败: %v", err)
|
||
}
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "737116531267"})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
want := "PDD 737116531267 · 秋冬保暖加绒卫衣"
|
||
if r.Rows[0].Target != want {
|
||
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_目标列_商品已软删除时退回显示商品ID(t *testing.T) {
|
||
db := newTestDB(t)
|
||
now := model.NowISO()
|
||
if _, err := db.Exec(`
|
||
INSERT INTO pdd_products (goods_id, url, title, collect_status, deleted_at, created_at, updated_at)
|
||
VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
|
||
'秋冬保暖加绒卫衣', 'collected', ?, ?, ?)`, now, now, now); err != nil {
|
||
t.Fatalf("插入 PDD 商品失败: %v", err)
|
||
}
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
want := "PDD 737116531267"
|
||
if r.Rows[0].Target != want {
|
||
t.Errorf("商品已软删除时应退回只显示商品 ID,实际 %q", r.Rows[0].Target)
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_目标列_采购任务显示订单号规格数量价格上限(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{
|
||
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned,
|
||
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
|
||
quantity: 2, maxPriceCent: 4200,
|
||
})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
want := "SO-001 · 黑色/M · 2件 · ≤¥42.00"
|
||
if r.Rows[0].Target != want {
|
||
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
|
||
}
|
||
}
|
||
|
||
// ── 客户端列 ──────────────────────────────────────────
|
||
|
||
func TestListTasksView_无主任务客户端列显示占位符(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if r.Rows[0].ClientText != "—" {
|
||
t.Errorf("无主任务客户端列应该显示占位符,实际 %q", r.Rows[0].ClientText)
|
||
}
|
||
}
|
||
|
||
// ── 统计跟随筛选 ──────────────────────────────────────
|
||
|
||
func TestListTasksView_统计跟随当前筛选(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "222"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "PUR-2", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-2"})
|
||
|
||
all, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if all.Total != 4 {
|
||
t.Fatalf("不筛选时总数应该是 4,实际 %d", all.Total)
|
||
}
|
||
|
||
collectOnly, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskCollect})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if collectOnly.Total != 2 {
|
||
t.Fatalf("筛选采集后总数应该是 2,实际 %d", collectOnly.Total)
|
||
}
|
||
if collectOnly.Counts[model.TaskAssigned] != 0 {
|
||
t.Errorf("筛选采集后不该统计到采购任务的'待领取',实际 %d", collectOnly.Counts[model.TaskAssigned])
|
||
}
|
||
if collectOnly.Counts[model.TaskPending] != 1 || collectOnly.Counts[model.TaskSucceeded] != 1 {
|
||
t.Errorf("筛选采集后的状态分布不对: %+v", collectOnly.Counts)
|
||
}
|
||
}
|
||
|
||
// ── 空状态 ────────────────────────────────────────────
|
||
|
||
func TestListTasksView_从未建过任务时不算筛选无结果(t *testing.T) {
|
||
db := newTestDB(t)
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(r.Rows) != 0 {
|
||
t.Fatalf("空库应该查出 0 条")
|
||
}
|
||
if r.IsFiltered {
|
||
t.Error("没有任何筛选条件时 IsFiltered 应该为 false,模板据此显示'还没有任务'而不是'筛选无结果'")
|
||
}
|
||
}
|
||
|
||
func TestListTasksView_筛选无结果时IsFiltered为true(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskPurchase})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(r.Rows) != 0 {
|
||
t.Fatalf("筛选采购应该查出 0 条")
|
||
}
|
||
if !r.IsFiltered {
|
||
t.Error("筛选出 0 条时 IsFiltered 应该为 true,模板据此显示'筛选无结果'而不是'还没有任务'")
|
||
}
|
||
}
|
||
|
||
// ── 详情弹窗 ──────────────────────────────────────────
|
||
|
||
func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{
|
||
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskClaimed,
|
||
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
|
||
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
|
||
claimedAt: model.NowISO(),
|
||
})
|
||
|
||
d, err := GetTaskDetail(db, "PUR-1")
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if d == nil {
|
||
t.Fatal("任务应该存在")
|
||
}
|
||
if !d.IsPurchase {
|
||
t.Error("采购任务 IsPurchase 应该为 true")
|
||
}
|
||
if d.SpecText != "黑色/M" || d.QuantityText != "2件" || d.PriceLimitText != "≤¥42.00" {
|
||
t.Errorf("采购专有字段不对: spec=%q qty=%q price=%q", d.SpecText, d.QuantityText, d.PriceLimitText)
|
||
}
|
||
if d.ClientText != "client-001" {
|
||
t.Errorf("分配客户端 = %q, want client-001", d.ClientText)
|
||
}
|
||
}
|
||
|
||
func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
|
||
d, err := GetTaskDetail(db, "COL-1")
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if d.IsPurchase {
|
||
t.Error("采集任务 IsPurchase 应该为 false,模板据此隐藏采购专有字段")
|
||
}
|
||
if d.SpecText != "" || d.QuantityText != "" || d.PriceLimitText != "" {
|
||
t.Errorf("采集任务不该填充采购专有字段: spec=%q qty=%q price=%q", d.SpecText, d.QuantityText, d.PriceLimitText)
|
||
}
|
||
}
|
||
|
||
func TestGetTaskDetail_有错误信息时显示(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{
|
||
taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskFailed,
|
||
pddGoodsID: "111", errorCode: "PDD_PAGE_TIMEOUT", errorMessage: "商品页加载超时",
|
||
})
|
||
|
||
d, err := GetTaskDetail(db, "COL-1")
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if !d.HasError {
|
||
t.Fatal("应该检测到错误信息")
|
||
}
|
||
if d.ErrorCode != "PDD_PAGE_TIMEOUT" || d.ErrorMessage != "商品页加载超时" {
|
||
t.Errorf("错误信息不对: code=%q msg=%q", d.ErrorCode, d.ErrorMessage)
|
||
}
|
||
}
|
||
|
||
func TestGetTaskDetail_没有错误信息时不显示错误段(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
|
||
d, err := GetTaskDetail(db, "COL-1")
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if d.HasError {
|
||
t.Error("没有错误码和错误说明时 HasError 应该为 false")
|
||
}
|
||
}
|
||
|
||
func TestGetTaskDetail_任务不存在返回nil(t *testing.T) {
|
||
db := newTestDB(t)
|
||
d, err := GetTaskDetail(db, "NOT-EXIST")
|
||
if err != nil {
|
||
t.Fatalf("不该报错: %v", err)
|
||
}
|
||
if d != nil {
|
||
t.Error("不存在的任务应该返回 nil")
|
||
}
|
||
}
|
||
|
||
// ── 删除 ──────────────────────────────────────────────
|
||
|
||
func TestDeleteTasks_批量删除(t *testing.T) {
|
||
db := newTestDB(t)
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
|
||
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "222"})
|
||
|
||
n, err := DeleteTasks(db, []string{"COL-1"})
|
||
if err != nil {
|
||
t.Fatalf("删除失败: %v", err)
|
||
}
|
||
if n != 1 {
|
||
t.Fatalf("应该删除 1 条,实际 %d", n)
|
||
}
|
||
|
||
r, err := ListTasksView(db, repository.TaskFilter{})
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-2" {
|
||
t.Fatalf("剩余任务应该只有 COL-2,实际 %+v", r.Rows)
|
||
}
|
||
}
|
||
|
||
// ── 目标列拼接的边界情况 ──────────────────────────────
|
||
|
||
func TestSpecText_空或无法解析时返回占位符(t *testing.T) {
|
||
if got := specText(""); got != placeholder {
|
||
t.Errorf("空 options: got %q", got)
|
||
}
|
||
if got := specText("not json"); got != placeholder {
|
||
t.Errorf("无法解析: got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestQuantityText_零或负数返回占位符(t *testing.T) {
|
||
if got := quantityText(0); got != placeholder+"件" {
|
||
t.Errorf("数量为 0: got %q", got)
|
||
}
|
||
if got := quantityText(3); got != "3件" {
|
||
t.Errorf("数量为 3: got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestPriceLimitText_零或负数返回占位符(t *testing.T) {
|
||
if got := priceLimitText(0); got != "≤"+placeholder {
|
||
t.Errorf("价格为 0: got %q", got)
|
||
}
|
||
if got := priceLimitText(4200); got != "≤¥42.00" {
|
||
t.Errorf("价格为 4200 分: got %q", got)
|
||
}
|
||
}
|