package api import ( "database/sql" "encoding/json" "io" "net/http" "strings" "time" ) type TemplatesAPI struct{ meta *sql.DB marketing *sql.DB } func TemplatesHandler(meta, marketing *sql.DB) http.Handler { api := &TemplatesAPI{meta: meta, marketing: marketing} return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/api/templates") if r.Method == http.MethodPost && p == "" { api.createTemplate(w, r) return } if r.Method == http.MethodGet && p == "" { api.listTemplates(w, r) return } if strings.HasPrefix(p, "/") { id := strings.TrimPrefix(p, "/") if r.Method == http.MethodGet { api.getTemplate(w, r, id) return } if r.Method == http.MethodPatch { api.patchTemplate(w, r, id) return } if r.Method == http.MethodDelete { api.deleteTemplate(w, r, id) return } if r.Method == http.MethodPost && strings.HasSuffix(p, "/validate") { id = strings.TrimSuffix(id, "/validate") api.validateTemplate(w, r, id) return } } w.WriteHeader(http.StatusNotFound) }) } type TemplatePayload struct{ Name string `json:"name"` Datasource string `json:"datasource"` MainTable string `json:"main_table"` Fields []string `json:"fields"` Filters map[string]interface{} `json:"filters"` FileFormat string `json:"file_format"` OwnerID uint64 `json:"owner_id"` Visibility string `json:"visibility"` } func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) var p TemplatePayload json.Unmarshal(b, &p) now := time.Now() _, err := a.meta.Exec( "INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, last_validated_at) VALUES (?,?,?,?,?,?,?,?,?,?)", p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), nil, p.FileFormat, p.Visibility, p.OwnerID, 1, now, ) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.WriteHeader(http.StatusCreated) w.Write([]byte("ok")) } func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) { rows, err := a.meta.Query("SELECT id,name,datasource,main_table,file_format,visibility,owner_id,enabled,explain_score,last_validated_at,created_at,updated_at FROM export_templates ORDER BY updated_at DESC LIMIT 200") if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } defer rows.Close() out := []map[string]interface{}{} for rows.Next() { var id uint64 var name, datasource, mainTable, fileFormat, visibility string var ownerID uint64 var enabled int var explainScore sql.NullInt64 var lastValidatedAt sql.NullTime var createdAt, updatedAt time.Time err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &explainScore, &lastValidatedAt, &createdAt, &updatedAt) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } m := map[string]interface{}{"id": id, "name": name, "datasource": datasource, "main_table": mainTable, "file_format": fileFormat, "visibility": visibility, "owner_id": ownerID, "enabled": enabled == 1, "explain_score": explainScore.Int64, "last_validated_at": lastValidatedAt.Time, "created_at": createdAt, "updated_at": updatedAt} out = append(out, m) } b, _ := json.Marshal(out) w.Header().Set("Content-Type", "application/json") w.Write(b) } func (a *TemplatesAPI) getTemplate(w http.ResponseWriter, r *http.Request, id string) { row := a.meta.QueryRow("SELECT id,name,datasource,main_table,fields_json,filters_json,file_format,visibility,owner_id,enabled,explain_score,last_validated_at,created_at,updated_at FROM export_templates WHERE id=?", id) var m = map[string]interface{}{} var tid uint64 var name, datasource, mainTable, fileFormat, visibility string var ownerID uint64 var enabled int var explainScore sql.NullInt64 var lastValidatedAt sql.NullTime var createdAt, updatedAt time.Time var fields, filters []byte err := row.Scan(&tid, &name, &datasource, &mainTable, &fields, &filters, &fileFormat, &visibility, &ownerID, &enabled, &explainScore, &lastValidatedAt, &createdAt, &updatedAt) if err != nil { w.WriteHeader(http.StatusNotFound) w.Write([]byte("not found")) return } m["id"] = tid m["name"] = name m["datasource"] = datasource m["main_table"] = mainTable m["file_format"] = fileFormat m["visibility"] = visibility m["owner_id"] = ownerID m["enabled"] = enabled == 1 m["explain_score"] = explainScore.Int64 m["last_validated_at"] = lastValidatedAt.Time m["created_at"] = createdAt m["updated_at"] = updatedAt m["fields"] = fromJSON(fields) m["filters"] = fromJSON(filters) b, _ := json.Marshal(m) w.Header().Set("Content-Type", "application/json") w.Write(b) } func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id string) { b, _ := io.ReadAll(r.Body) var p map[string]interface{} json.Unmarshal(b, &p) set := []string{} args := []interface{}{} for k, v := range p { switch k { case "name", "visibility", "file_format": set = append(set, k+"=?") args = append(args, v) case "enabled": set = append(set, "enabled=?") if v.(bool) { args = append(args, 1) } else { args = append(args, 0) } } } if len(set) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("no patch")) return } args = append(args, id) _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id=?", args...) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.Write([]byte("ok")) } func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) { var cnt int64 row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id=?", id) _ = row.Scan(&cnt) if cnt > 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("template in use")) return } _, err := a.meta.Exec("DELETE FROM export_templates WHERE id=?", id) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.Write([]byte("ok")) } func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, id string) { row := a.meta.QueryRow("SELECT main_table, fields_json, filters_json FROM export_templates WHERE id=?", id) var main string var fields, filters []byte err := row.Scan(&main, &fields, &filters) if err != nil { w.WriteHeader(http.StatusNotFound) w.Write([]byte("not found")) return } var fs []string var fl map[string]interface{} json.Unmarshal(fields, &fs) json.Unmarshal(filters, &fl) // 模板不再记录 EXPLAIN,返回成功 w.Write([]byte("ok")) } func toJSON(v interface{}) []byte { b, _ := json.Marshal(v) return b } func fromJSON(b []byte) interface{} { var v interface{} json.Unmarshal(b, &v) return v } func whitelist() map[string]bool { m := map[string]bool{ "order.order_number": true, "order.creator": true, "order.out_trade_no": true, "order.type": true, "order.status": true, "order.contract_price": true, "order.num": true, "order.total": true, "order.pay_amount": true, "order.create_time": true, "order.update_time": true, "order_detail.plan_title": true, "order_detail.reseller_name": true, "order_detail.product_name": true, "order_detail.show_url": true, "order_detail.official_price": true, "order_detail.cost_price": true, "order_detail.create_time": true, "order_detail.update_time": true, "order_cash.channel": true, "order_cash.cash_activity_id": true, "order_cash.receive_status": true, "order_cash.receive_time": true, "order_cash.cash_packet_id": true, "order_cash.cash_id": true, "order_cash.amount": true, "order_cash.status": true, "order_cash.expire_time": true, "order_cash.update_time": true, "order_voucher.channel": true, "order_voucher.channel_activity_id": true, "order_voucher.channel_voucher_id": true, "order_voucher.status": true, "order_voucher.grant_time": true, "order_voucher.usage_time": true, "order_voucher.refund_time": true, "order_voucher.status_modify_time": true, "order_voucher.overdue_time": true, "order_voucher.refund_amount": true, "order_voucher.official_price": true, "order_voucher.out_biz_no": true, "order_voucher.account_no": true, "plan.id": true, "plan.title": true, "plan.status": true, "plan.begin_time": true, "plan.end_time": true, "key_batch.id": true, "key_batch.batch_name": true, "key_batch.bind_object": true, "key_batch.quantity": true, "key_batch.stock": true, "key_batch.begin_time": true, "key_batch.end_time": true, "code_batch.id": true, "code_batch.title": true, "code_batch.status": true, "code_batch.begin_time": true, "code_batch.end_time": true, "code_batch.quantity": true, "code_batch.usage": true, "code_batch.stock": true, "voucher.channel": true, "voucher.channel_activity_id": true, "voucher.price": true, "voucher.balance": true, "voucher.used_amount": true, "voucher.denomination": true, "voucher_batch.channel_activity_id": true, "voucher_batch.temp_no": true, "voucher_batch.provider": true, "voucher_batch.weight": true, "merchant_key_send.merchant_id": true, "merchant_key_send.out_biz_no": true, "merchant_key_send.key": true, "merchant_key_send.status": true, "merchant_key_send.usage_time": true, "merchant_key_send.create_time": true, } return m }