package api import ( "database/sql" "encoding/json" "io" "log" "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 } } fail(w, r, http.StatusNotFound, "not found") }) } 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) r = WithPayload(r, p) now := time.Now() tplSQL := "INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, stats_enabled, last_validated_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" tplArgs := []interface{}{p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), toJSON(p.Filters), p.FileFormat, p.Visibility, p.OwnerID, 1, 0, now, now, now} log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs) _, err := a.meta.Exec(tplSQL, tplArgs...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } writeJSON(w, r, http.StatusCreated, 0, "ok", nil) } 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,last_validated_at,created_at,updated_at, COALESCE(JSON_LENGTH(fields_json),0) AS field_count, (SELECT COUNT(1) FROM export_jobs ej WHERE ej.template_id = export_templates.id) AS exec_count FROM export_templates ORDER BY updated_at DESC LIMIT 200") if err != nil { fail(w, r, http.StatusInternalServerError, 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 lastValidatedAt sql.NullTime var createdAt, updatedAt time.Time var fieldCount, execCount int64 err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldCount, &execCount) if err != nil { fail(w, r, http.StatusInternalServerError, 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, "last_validated_at": lastValidatedAt.Time, "created_at": createdAt, "updated_at": updatedAt, "field_count": fieldCount, "exec_count": execCount} out = append(out, m) } ok(w, r, out) } 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 { fail(w, r, http.StatusNotFound, "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) ok(w, r, m) } 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 { fail(w, r, http.StatusBadRequest, "no patch") return } args = append(args, id) _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } ok(w, r, nil) } 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 { fail(w, r, http.StatusBadRequest, "template in use") return } _, err := a.meta.Exec("DELETE FROM export_templates WHERE id= ?", id) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } ok(w, r, nil) } 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 { fail(w, r, http.StatusNotFound, "not found") return } var fs []string var fl map[string]interface{} json.Unmarshal(fields, &fs) json.Unmarshal(filters, &fl) ok(w, r, nil) } 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 } func fieldLabels() map[string]string { return map[string]string{ "order.order_number": "订单编号", "order.creator": "创建者ID", "order.out_trade_no": "支付流水号", "order.type": "订单类型", "order.status": "订单状态", "order.contract_price": "合同单价", "order.num": "数量", "order.total": "总金额", "order.pay_amount": "支付金额", "order.create_time": "创建时间", "order.update_time": "更新时间", "order_detail.plan_title": "计划标题", "order_detail.reseller_name": "分销商名称", "order_detail.product_name": "商品名称", "order_detail.show_url": "商品图片URL", "order_detail.official_price": "官方价", "order_detail.cost_price": "成本价", "order_detail.create_time": "创建时间", "order_detail.update_time": "更新时间", "order_cash.channel": "渠道", "order_cash.cash_activity_id": "红包批次号", "order_cash.receive_status": "领取状态", "order_cash.receive_time": "拆红包时间", "order_cash.cash_packet_id": "红包ID", "order_cash.cash_id": "红包规则ID", "order_cash.amount": "红包额度", "order_cash.status": "状态", "order_cash.expire_time": "过期时间", "order_cash.update_time": "更新时间", "order_voucher.channel": "渠道", "order_voucher.channel_activity_id": "渠道立减金批次", "order_voucher.channel_voucher_id": "渠道立减金ID", "order_voucher.status": "状态", "order_voucher.grant_time": "领取时间", "order_voucher.usage_time": "核销时间", "order_voucher.refund_time": "退款时间", "order_voucher.status_modify_time": "状态更新时间", "order_voucher.overdue_time": "过期时间", "order_voucher.refund_amount": "退款金额", "order_voucher.official_price": "官方价", "order_voucher.out_biz_no": "外部业务号", "order_voucher.account_no": "账户号", "plan.id": "计划ID", "plan.title": "计划标题", "plan.status": "状态", "plan.begin_time": "开始时间", "plan.end_time": "结束时间", "key_batch.id": "批次ID", "key_batch.batch_name": "批次名称", "key_batch.bind_object": "绑定对象", "key_batch.quantity": "发放数量", "key_batch.stock": "剩余库存", "key_batch.begin_time": "开始时间", "key_batch.end_time": "结束时间", "code_batch.id": "兑换批次ID", "code_batch.title": "标题", "code_batch.status": "状态", "code_batch.begin_time": "开始时间", "code_batch.end_time": "结束时间", "code_batch.quantity": "数量", "code_batch.usage": "使用数", "code_batch.stock": "库存", "voucher.channel": "渠道", "voucher.channel_activity_id": "渠道批次号", "voucher.price": "合同单价", "voucher.balance": "剩余额度", "voucher.used_amount": "已用额度", "voucher.denomination": "面额", "voucher_batch.channel_activity_id": "渠道批次号", "voucher_batch.temp_no": "模板编号", "voucher_batch.provider": "服务商", "voucher_batch.weight": "权重", "merchant_key_send.merchant_id": "商户ID", "merchant_key_send.out_biz_no": "商户业务号", "merchant_key_send.key": "券码", "merchant_key_send.status": "状态", "merchant_key_send.usage_time": "核销时间", "merchant_key_send.create_time": "创建时间", } }