整改一期:发布这条链路先跑顺
四条改动都在"发布一个代喂任务"这条路上: 定位拿不到不再把人卡死。地址可以不带坐标保存,界面标成"未核验定位", 这条地址的打卡照常,只是不比对距离——总比逼着用户放弃发布要好。客户端 另外给了两条退路:去系统设置开权限,或者地图选点。 价格从写死的 30 元改成服务端按物种、城市、每次时长算的推荐价,规则放在 配置里,运营改一次不用等发版;用户改过之后就以他填的为准,不再被覆盖。 发布与支付并成一步:底部常驻"合计 ¥x = n 次 × ¥y",主按钮是"支付并发布"。 没付成的任务留在"我的任务"里能接着付,超过 24 小时由后台关掉,不再变成 既看不见又删不掉的孤儿。 选宠物换成带头像的卡片,缺免疫记录或照片的当场标出来,不用等提交才知道。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9345805133
commit
ebf7189533
@@ -24,12 +24,13 @@ func TestPetFeedTaskMySQL(t *testing.T) {
|
||||
petID := petWithPhotos(t, a, owner, "咪咪", 3)
|
||||
|
||||
var addressID int64
|
||||
t.Run("an address without a location is refused", func(t *testing.T) {
|
||||
t.Run("an address keeps its door number encrypted", func(t *testing.T) {
|
||||
// 定位拿不到也要能存下来,只是这条地址此后打卡不做距离核验。
|
||||
w := httptest.NewRecorder()
|
||||
a.createPetAddress(w, imTestRequest(owner, "POST", "/api/v1/pet/addresses",
|
||||
`{"cityName":"深圳","areaText":"南山区科技园","detail":"3 栋 1201","contact":"张先生 13800000000"}`))
|
||||
if w.Code != 400 {
|
||||
t.Fatalf("没有定位的地址应当被拒绝,打卡要靠它比对: HTTP %d %s", w.Code, w.Body.String())
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), `"located":false`) {
|
||||
t.Fatalf("没有定位也该能保存并标记未核验: HTTP %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
ok := httptest.NewRecorder()
|
||||
a.createPetAddress(ok, imTestRequest(owner, "POST", "/api/v1/pet/addresses",
|
||||
@@ -898,3 +899,113 @@ func TestPetAddressLifecycleMySQL(t *testing.T) {
|
||||
t.Fatalf("清空后的地址不该能继续下单: HTTP %d %s", reuse.Code, reuse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 一期的三条服务端约定:定位拿不到也要能存地址、推荐价按物种与城市给、
|
||||
// 没付款的任务不会永远挂在那里。
|
||||
func TestPetFeedPhaseOneMySQL(t *testing.T) {
|
||||
db := isolatedIMDatabase(t)
|
||||
a := &App{db: db, hub: NewHub(), config: Config{Environment: "development", JWTSecret: "isolated-im-regression-secret-only"}}
|
||||
a.config.ConfigEncryptionKey = strings.Repeat("k", 32)
|
||||
owner := int64(1)
|
||||
|
||||
t.Run("an address saves without coordinates and says so", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
a.createPetAddress(w, imTestRequest(owner, "POST", "/api/v1/pet/addresses",
|
||||
`{"cityCode":"440300","cityName":"深圳","areaText":"南山区科技园","detail":"3 栋 1201","contact":"张先生 13800000000"}`))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("定位失败不该挡住地址保存: HTTP %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
ID int64 `json:"id"`
|
||||
Located bool `json:"located"`
|
||||
} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &envelope)
|
||||
if envelope.Data.Located {
|
||||
t.Fatal("没有坐标就不能说自己定位过")
|
||||
}
|
||||
var latitude sql.NullFloat64
|
||||
if err := db.QueryRow(`SELECT latitude FROM pet_addresses WHERE id=?`, envelope.Data.ID).Scan(&latitude); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if latitude.Valid {
|
||||
t.Fatal("坐标应当留空,而不是写成 0")
|
||||
}
|
||||
var list struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(imTestCall(t, a.petAddresses, owner, "GET", "/api/v1/pet/addresses", "", 200), &list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(list.Items) != 1 || list.Items[0]["located"] != false || list.Items[0]["cityCode"] != "440300" {
|
||||
t.Fatalf("地址列表要带上城市码与核验状态: %+v", list.Items)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the suggested price follows species and city", func(t *testing.T) {
|
||||
read := func(query string) map[string]any {
|
||||
t.Helper()
|
||||
var view map[string]any
|
||||
if err := json.Unmarshal(imTestCall(t, a.feedPriceSuggestion, owner, "GET",
|
||||
"/api/v1/pet/feed-price"+query, "", 200), &view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return view
|
||||
}
|
||||
cat := read("?species=cat&cityCode=440300&minMinutes=20")
|
||||
dog := read("?species=dog&cityCode=440300&minMinutes=20")
|
||||
small := read("?species=cat&cityCode=620100&minMinutes=20")
|
||||
longer := read("?species=cat&cityCode=440300&minMinutes=45")
|
||||
if dog["priceCent"].(float64) <= cat["priceCent"].(float64) {
|
||||
t.Fatalf("狗要遛,单价应当高于猫: %v vs %v", dog["priceCent"], cat["priceCent"])
|
||||
}
|
||||
if small["priceCent"].(float64) >= cat["priceCent"].(float64) {
|
||||
t.Fatalf("没有系数的城市不该比深圳贵: %v vs %v", small["priceCent"], cat["priceCent"])
|
||||
}
|
||||
if longer["priceCent"].(float64) <= cat["priceCent"].(float64) {
|
||||
t.Fatalf("每次待更久应当更贵: %v", longer["priceCent"])
|
||||
}
|
||||
// 推荐价必须落在服务端自己会接受的区间里,否则填进去就提交不了。
|
||||
for _, view := range []map[string]any{cat, dog, small, longer} {
|
||||
price := view["priceCent"].(float64)
|
||||
if price < view["minPriceCent"].(float64) || price > view["maxPriceCent"].(float64) {
|
||||
t.Fatalf("推荐价超出可提交区间: %+v", view)
|
||||
}
|
||||
}
|
||||
// 认识的物种要按自己的档位走,不认识的落到 other。
|
||||
unknown := read("?species=dragon&cityCode=440300")
|
||||
if unknown["species"] != "other" {
|
||||
t.Fatalf("未知物种应当归到 other: %v", unknown["species"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unpaid tasks are closed after the grace period", func(t *testing.T) {
|
||||
taskID := seedTaskAwaitingPayment(t, a, db, owner)
|
||||
if closed := a.closeStaleUnpaidTasks(context.Background()); closed != 0 {
|
||||
t.Fatalf("刚建的任务不该被关掉: %d", closed)
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE pet_feed_tasks SET created_at=DATE_SUB(NOW(3),INTERVAL 30 HOUR) WHERE id=?`, taskID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if closed := a.closeStaleUnpaidTasks(context.Background()); closed != 1 {
|
||||
t.Fatalf("超过 24 小时未支付应当关闭: %d", closed)
|
||||
}
|
||||
var status string
|
||||
_ = db.QueryRow(`SELECT status FROM pet_feed_tasks WHERE id=?`, taskID).Scan(&status)
|
||||
if status != "CANCELLED" {
|
||||
t.Fatalf("状态 = %s", status)
|
||||
}
|
||||
// 已经付过款的任务不受影响。
|
||||
paid := seedTaskAwaitingPayment(t, a, db, owner)
|
||||
payFeedTask(t, a, owner, paid)
|
||||
if _, err := db.Exec(`UPDATE pet_feed_tasks SET created_at=DATE_SUB(NOW(3),INTERVAL 30 HOUR) WHERE id=?`, paid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.closeStaleUnpaidTasks(context.Background())
|
||||
_ = db.QueryRow(`SELECT status FROM pet_feed_tasks WHERE id=?`, paid).Scan(&status)
|
||||
if status != "ESCROWED" {
|
||||
t.Fatalf("已托管的任务被误关了: %s", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user