MarketingSystemDataTool/server/internal/api/templates.go

314 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}